From ee207c978faa2852c17ae07cc2d5cdc5091ec031 Mon Sep 17 00:00:00 2001 From: dakhnod Date: Sun, 4 Sep 2022 23:05:57 +0200 Subject: [PATCH] Device FlipperZero: added basic support for the Flipper Zero (#2840) This PR adds support for the flipper zero device. It's main purpose currently is to provide an Intent-based API to Tasker and similar apps to play sub-GHz files. In the future, file management and other features might be useful. Co-authored-by: Daniel Dakhno Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2840 Co-authored-by: dakhnod Co-committed-by: dakhnod --- .../gadgetbridge/devices/DeviceManager.java | 9 + .../flipper/zero/FlipperZeroCoordinator.java | 118 ++++++++++ .../gadgetbridge/model/DeviceType.java | 1 + .../service/DeviceCommunicationService.java | 97 +++++++- .../service/DeviceSupportFactory.java | 3 + .../gadgetbridge/service/btle/BtLEQueue.java | 2 + .../btle/actions/RequestMtuAction.java | 2 +- .../zero/support/FlipperZeroBaseSupport.java | 179 +++++++++++++++ .../zero/support/FlipperZeroSupport.java | 209 ++++++++++++++++++ .../gadgetbridge/util/DeviceHelper.java | 2 + .../gadgetbridge/util/GBPrefs.java | 1 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/preferences.xml | 5 + 13 files changed, 627 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/flipper/zero/FlipperZeroCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroBaseSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroSupport.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java index d6bc7cd35..b607af8df 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java @@ -175,6 +175,15 @@ public class DeviceManager { return Collections.unmodifiableList(deviceList); } + public GBDevice getDeviceByAddress(String address){ + for(GBDevice device : deviceList){ + if(device.getAddress().compareToIgnoreCase(address) == 0){ + return device; + } + } + return null; + } + public List getSelectedDevices() { return selectedDevices; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/flipper/zero/FlipperZeroCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/flipper/zero/FlipperZeroCoordinator.java new file mode 100644 index 000000000..29f7044a8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/flipper/zero/FlipperZeroCoordinator.java @@ -0,0 +1,118 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class FlipperZeroCoordinator extends AbstractBLEDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + if(candidate.supportsService(UUID.fromString("00003082-0000-1000-8000-00805f9b34fb"))){ + return DeviceType.FLIPPER_ZERO; // need to filter for flipper here + } + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.FLIPPER_ZERO; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public int getAlarmSlotCount() { + return 0; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public String getManufacturer() { + return "Flipper devices"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_NONE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index ebda0e846..573b6b77c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -115,6 +115,7 @@ public enum DeviceType { VESC_NRF(500, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_vesc), VESC_HM10(501, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_vesc), BINARY_SENSOR(510, R.drawable.ic_device_unknown, R.drawable.ic_device_unknown_disabled, R.string.devicetype_binary_sensor), + FLIPPER_ZERO(520, R.drawable.ic_device_unknown, R.drawable.ic_device_unknown_disabled, R.string.devicetype_flipper_zero), TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test); private final int key; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 631ed58bb..9a410026f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -32,6 +32,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.location.Location; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.widget.Toast; @@ -53,6 +54,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver; @@ -337,6 +339,68 @@ public class DeviceCommunicationService extends Service implements SharedPrefere "com.spotify.music.playbackstatechanged" }; + private final String COMMAND_BLUETOOTH_CONNECT = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECT"; + private final String ACTION_DEVICE_CONNECTED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECTED"; + private boolean allowBluetoothIntentApi = false; + + private void sendDeviceConnectedBroadcast(String address){ + if(!allowBluetoothIntentApi){ + GB.log("not sending API event due to settings", GB.INFO, null); + return; + } + Intent intent = new Intent(ACTION_DEVICE_CONNECTED); + intent.putExtra("EXTRA_DEVICE_ADDRESS", address); + + sendBroadcast(intent); + } + + BroadcastReceiver bluetoothCommandReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()){ + case COMMAND_BLUETOOTH_CONNECT: + if(!allowBluetoothIntentApi){ + GB.log("Connection API not allowed in settings", GB.ERROR, null); + return; + } + Bundle extras = intent.getExtras(); + if(extras == null){ + GB.log("no extras provided in Intent", GB.ERROR, null); + return; + } + String address = extras.getString("EXTRA_DEVICE_ADDRESS", ""); + if(address.isEmpty()){ + GB.log("no bluetooth address provided in Intent", GB.ERROR, null); + return; + } + if(isDeviceConnected(address)){ + GB.log(String.format("device %s already connected", address), GB.INFO, null); + sendDeviceConnectedBroadcast(address); + return; + } + + List devices = GBApplication.app().getDeviceManager().getDevices(); + GBDevice targetDevice = GBApplication + .app() + .getDeviceManager() + .getDeviceByAddress(address); + + if(targetDevice == null){ + GB.log(String.format("device %s not registered", address), GB.ERROR, null); + return; + } + + GB.log(String.format("connecting to %s", address), GB.INFO, null); + + GBApplication + .deviceService(targetDevice) + .connect(); + + break; + } + } + }; + /** * For testing! * @@ -366,6 +430,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere cachedStruct.setCoordinator(newCoordinator); } updateReceiversState(); + + if(device.isInitialized()){ + sendDeviceConnectedBroadcast(device.getAddress()); + } } } }; @@ -412,7 +480,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere if (hasPrefs()) { getPrefs().getPreferences().registerOnSharedPreferenceChangeListener(this); + allowBluetoothIntentApi = getPrefs().getBoolean(GBPrefs.PREF_ALLOW_INTENT_API, false); } + + IntentFilter bluetoothCommandFilter = new IntentFilter(); + bluetoothCommandFilter.addAction(COMMAND_BLUETOOTH_CONNECT); + registerReceiver(bluetoothCommandReceiver, bluetoothCommandFilter); } private DeviceSupportFactory getDeviceSupportFactory() { @@ -957,8 +1030,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } private boolean isDeviceConnected(GBDevice device) { + return isDeviceConnected(device.getAddress()); + } + + private boolean isDeviceConnected(String deviceAddress) { for(DeviceStruct struct : deviceStructs){ - if(struct.getDevice().equals(device) ){ + if(struct.getDevice().getAddress().compareToIgnoreCase(deviceAddress) == 0){ return struct.getDevice().isConnected(); } } @@ -966,8 +1043,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } private boolean isDeviceConnecting(GBDevice device) { + return isDeviceConnecting(device.getAddress()); + } + + private boolean isDeviceConnecting(String deviceAddress) { for(DeviceStruct struct : deviceStructs){ - if(struct.getDevice().equals(device) ){ + if(struct.getDevice().getAddress().compareToIgnoreCase(deviceAddress) == 0){ return struct.getDevice().isConnecting(); } } @@ -975,8 +1056,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } private boolean isDeviceInitialized(GBDevice device) { + return isDeviceInitialized(device.getAddress()); + } + + private boolean isDeviceInitialized(String deviceAddress) { for(DeviceStruct struct : deviceStructs){ - if(struct.getDevice().equals(device) ){ + if(struct.getDevice().getAddress().compareToIgnoreCase(deviceAddress) == 0){ return struct.getDevice().isInitialized(); } } @@ -1188,6 +1273,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } } GB.removeNotification(GB.NOTIFICATION_ID, this); // need to do this because the updated notification won't be cancelled when service stops + + unregisterReceiver(bluetoothCommandReceiver); } @Override @@ -1206,6 +1293,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere if (GBPrefs.CHART_MAX_HEART_RATE.equals(key) || GBPrefs.CHART_MIN_HEART_RATE.equals(key)) { HeartRateUtils.getInstance().updateCachedHeartRatePreferences(); } + if (GBPrefs.PREF_ALLOW_INTENT_API.equals(key)){ + allowBluetoothIntentApi = sharedPreferences.getBoolean(GBPrefs.PREF_ALLOW_INTENT_API, false); + GB.log("allowBluetoothIntentApi changed to " + allowBluetoothIntentApi, GB.INFO, null); + } } protected boolean hasPrefs() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 0daeb73da..c0f1d9b14 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDev import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioGB6900DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioGBX100DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.domyos.DomyosT540Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.flipper.zero.support.FlipperZeroSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.galaxy_buds.GalaxyBudsDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; @@ -324,6 +325,8 @@ public class DeviceSupportFactory { return new ServiceDeviceSupport(new QC35BaseSupport()); case BINARY_SENSOR: return new ServiceDeviceSupport(new BinarySensorSupport()); + case FLIPPER_ZERO: + return new ServiceDeviceSupport(new FlipperZeroSupport()); } return null; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index 5e3388ede..ce3c819d7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java @@ -550,6 +550,8 @@ public final class BtLEQueue { if(getCallbackToUse() != null){ getCallbackToUse().onMtuChanged(gatt, mtu, status); } + + mWaitForActionResultLatch.countDown(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/RequestMtuAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/RequestMtuAction.java index 7e9ead2d1..657176f21 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/RequestMtuAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/RequestMtuAction.java @@ -36,7 +36,7 @@ public class RequestMtuAction extends BtLEAction { @Override public boolean expectsResult() { - return false; + return true; } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroBaseSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroBaseSupport.java new file mode 100644 index 000000000..6ed6ea1cb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroBaseSupport.java @@ -0,0 +1,179 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.flipper.zero.support; + +import android.net.Uri; + +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; + +public class FlipperZeroBaseSupport extends AbstractBTLEDeviceSupport { + public FlipperZeroBaseSupport() { + super(LoggerFactory.getLogger(FlipperZeroBaseSupport.class)); + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(CallSpec callSpec) { + + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config, Integer id) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchRecordedData(int dataTypes) { + + } + + @Override + public void onReset(int flags) { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onReadConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + @Override + public boolean useAutoConnect() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroSupport.java new file mode 100644 index 000000000..57c1b41bd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/flipper/zero/support/FlipperZeroSupport.java @@ -0,0 +1,209 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.flipper.zero.support; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.widget.Toast; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class FlipperZeroSupport extends FlipperZeroBaseSupport{ + private BatteryInfoProfile batteryInfoProfile = new BatteryInfoProfile(this); + + private final String UUID_SERIAL_SERVICE = "8fe5b3d5-2e7f-4a98-2a48-7acc60fe0000"; + private final String UUID_SERIAL_CHARACTERISTIC_WRITE = "19ed82ae-ed21-4c9d-4145-228e62fe0000"; + private final String UUID_SERIAL_CHARACTERISTIC_RESPONSE = "19ed82ae-ed21-4c9d-4145-228e61fe0000"; + + private final String COMMAND_PLAY_FILE = "nodomain.freeyourgadget.gadgetbridge.flipper.zero.PLAY_FILE"; + private final String ACTION_PLAY_DONE = "nodomain.freeyourgadget.gadgetbridge.flipper.zero.PLAY_DONE"; + + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, final Intent intent) { + new Thread(new Runnable() { + @Override + public void run() { + if(COMMAND_PLAY_FILE.equals(intent.getAction())){ + handlePlaySubGHZ(intent); + } + } + }).start(); + } + }; + + private void handlePlaySubGHZ(Intent intent) { + String appName = intent.getExtras().getString("EXTRA_APP_NAME", "Sub-GHz"); + + String filePath = intent.getStringExtra("EXTRA_FILE_PATH"); + if(filePath == null){ + GB.log("missing EXTRA_FILE_PATH in intent", GB.ERROR, null); + return; + } + if(filePath.isEmpty()){ + GB.log("empty EXTRA_FILE_PATH in intent", GB.ERROR, null); + return; + } + + GB.toast(String.format("playing %s file", appName), Toast.LENGTH_SHORT, GB.INFO); + playFile(appName, filePath); + + Intent response = new Intent(ACTION_PLAY_DONE); + getContext().sendBroadcast(response); + } + + public FlipperZeroSupport() { + super(); + + batteryInfoProfile.addListener(new IntentListener() { + @Override + public void notify(Intent intent) { + BatteryInfo info = intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO); + GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo(); + batteryEvent.state = BatteryState.BATTERY_NORMAL; + batteryEvent.level = info.getPercentCharged(); + evaluateGBDeviceEvent(batteryEvent); + } + }); + addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE); + addSupportedProfile(batteryInfoProfile); + + addSupportedService(UUID.fromString(UUID_SERIAL_SERVICE)); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + getContext().registerReceiver(receiver, new IntentFilter(COMMAND_PLAY_FILE)); + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + batteryInfoProfile.requestBatteryInfo(builder); + batteryInfoProfile.enableNotify(builder, true); + + return builder + .notify(getCharacteristic(UUID.fromString(UUID_SERIAL_CHARACTERISTIC_RESPONSE)), true) + .requestMtu(512) + .add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + } + + @Override + public void dispose() { + super.dispose(); + + getContext().unregisterReceiver(receiver); + } + + private void sendSerialData(byte[] data){ + new TransactionBuilder("send serial data") + .write(getCharacteristic(UUID.fromString(UUID_SERIAL_CHARACTERISTIC_WRITE)), data) + .queue(getQueue()); + } + + private void sendProtobufPacket(byte[] packet){ + byte[] fullPacket = new byte[packet.length + 1]; + fullPacket[0] = (byte) packet.length; + System.arraycopy(packet, 0, fullPacket, 1, packet.length); + sendSerialData(fullPacket); + } + + private void openApp(String appName){ + // sub ghz payload: 13-08-15-82-01-0E-0A-07- 53-75-62-2D-47-48-7A -12-03-52-50-43 + ByteBuffer buffer = ByteBuffer.allocate(12 + appName.length()); + buffer.put((byte)0x08); + buffer.put((byte)0x15); + // buffer.put((byte)(Math.random() * 256)); + buffer.put((byte)0x82); + buffer.put((byte)0x01); + buffer.put((byte) (appName.length() + 7)); + buffer.put((byte)0x0A); + + buffer.put((byte) appName.length()); + buffer.put(appName.getBytes()); + + buffer.put((byte)0x12); + buffer.put((byte)0x03); + buffer.put((byte)0x52); + buffer.put((byte)0x50); + buffer.put((byte)0x43); + + sendProtobufPacket(buffer.array()); + } + + private void openSubGhzApp(){ + openApp("Sub-GHz"); + } + + private void appLoadFile(String filePath){ + // example payload 1C-08-16-82-03-17-0A-15- 2F-61-6E-79-2F-73-75-62-67-68-7A-2F-74-65-73-6C-61-2E-73-75-62 + ByteBuffer buffer = ByteBuffer.allocate(7 + filePath.length()); + + buffer.put((byte) 0x08); + buffer.put((byte) 0x16); + buffer.put((byte) 0x82); + buffer.put((byte) 0x03); + buffer.put((byte) (filePath.length() + 2)); + buffer.put((byte) 0x0A); + buffer.put((byte) filePath.length()); + buffer.put(filePath.getBytes()); + + sendProtobufPacket(buffer.array()); + } + + private void appButtonPress(){ + sendProtobufPacket(new byte[]{ + (byte) 0x08, (byte) 0x17, (byte) 0x8A, (byte) 0x03, (byte) 0x00} + ); + } + + private void appButtonRelease(){ + sendProtobufPacket(new byte[]{ + (byte) 0x08, (byte) 0x18, (byte) 0x92, (byte) 0x03, (byte) 0x00} + ); + } + private void appExitRequest(){ + sendProtobufPacket(new byte[]{ + (byte) 0x08, (byte) 0x19, (byte) 0xFA, (byte) 0x02, (byte) 0x00 + }); + } + + @Override + public void onTestNewFunction() { + openApp("Infrared"); + } + + private void playFile(String appName, String filePath){ + openApp(appName); + try { + Thread.sleep(500); + appLoadFile(filePath); + Thread.sleep(500); + appButtonPress(); + Thread.sleep(500); + appButtonRelease(); + Thread.sleep(1000); + appExitRequest(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public void onFetchRecordedData(int dataTypes) { + super.onFetchRecordedData(dataTypes); + + onTestNewFunction(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index e92444400..aea80f1dd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -49,6 +49,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Cooridnator; +import nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero.FlipperZeroCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsLiveDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsProDeviceCoordinator; @@ -335,6 +336,7 @@ public class DeviceHelper { result.add(new SonyWF1000XM3Coordinator()); result.add(new QC35Coordinator()); result.add(new BinarySensorCoordinator()); + result.add(new FlipperZeroCoordinator()); return result; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java index 0ab53e341..50fd51461 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java @@ -56,6 +56,7 @@ public class GBPrefs { public static final String RTL_SUPPORT = "rtl"; public static final String RTL_CONTEXTUAL_ARABIC = "contextualArabic"; public static boolean AUTO_RECONNECT_DEFAULT = true; + public static final String PREF_ALLOW_INTENT_API = "prefs_key_allow_bluetooth_intent_api"; public static final String USER_NAME = "mi_user_alias"; public static final String USER_NAME_DEFAULT = "gadgetbridge-user"; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55a98a853..d845d3e6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1813,4 +1813,7 @@ Steps Achievements Hourly chime The watch will beep once an hour + Flipper zero + Bluetooth Intent API + Allow controlling Bluetooth connection via Intent API diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b7c26a48d..d14fbc0ec 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -386,5 +386,10 @@ android:key="pref_discovery_pairing" android:title="@string/activity_prefs_discovery_pairing" /> + +