From 3ef57bf5fd44b8a58775ec11700e2e61f1ef194e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylwester=20Zieli=C5=84ski?= Date: Fri, 24 Sep 2021 10:25:10 +0200 Subject: [PATCH] Modernization of modular approach --- app/build.gradle | 4 +- build.gradle | 2 +- feature_csc/build.gradle | 6 +- .../android/csc/events/CSCServiceEvent.kt | 4 +- .../csc/service/BleProfileService.java | 613 ------------------ .../csc/service/CSCDataReadBroadcast.kt | 18 +- .../android/csc/service/CSCManager.kt | 15 +- .../csc/service/CSCManagerCallbacks.kt | 1 + .../CSCMeasurementParser.kt | 2 +- .../android/csc/service/CSCService.kt | 170 +---- .../android/csc/view/CSCSettings.kt | 9 + .../android/csc/view/CSCViewConnectedState.kt | 4 +- .../nordicsemi/android/csc/view/CscScreen.kt | 22 +- .../csc/{view => viewmodel}/CscViewModel.kt | 43 +- feature_csc/src/main/res/values/strings.xml | 6 - {lib_scanner => feature_scanner}/build.gradle | 2 +- .../scanner/ExampleInstrumentedTest.kt | 0 .../src/main/AndroidManifest.xml | 0 .../nordicsemi/android/scanner/HiltModule.kt | 30 + .../android/scanner/ScannerNavigation.kt | 13 +- .../android/scanner/tools/NordicBleScanner.kt | 34 - .../android/scanner/tools/ScannerStatus.kt | 2 +- .../tools/SelectedBluetoothDeviceHolder.kt | 25 + .../view}/BluetoothNotAvailableScreen.kt | 2 +- .../scanner/view}/RequestPermissionScreen.kt | 11 +- .../android/scanner/view}/ScanDeviceScreen.kt | 21 +- .../viewmodel}/NordicBleScannerViewModel.kt | 19 +- .../src/main/res/values/strings.xml | 0 .../android/scanner/ExampleUnitTest.kt | 0 lib_broadcast/build.gradle | 7 - lib_broadcast/src/main/AndroidManifest.xml | 4 - lib_events/build.gradle | 3 - .../android/events/BluetoothReadDataEvent.kt | 3 - .../java/no/nordicsemi/android/events/Ext.kt | 7 - .../nordicsemi/android/scanner/HiltModule.kt | 17 - lib_service/build.gradle | 18 + .../service}/ExampleInstrumentedTest.kt | 4 +- lib_service/src/main/AndroidManifest.xml | 5 + .../android/service}/BatteryManager.kt | 5 +- .../service/BatteryManagerCallbacks.kt | 2 +- .../android/service/BleProfileService.kt | 554 ++++++++++++++++ .../service/BluetoothDataReadBroadcast.kt | 19 + .../android/service/ForegroundBleService.kt | 121 ++++ .../android/service}/LoggableBleManager.kt | 14 +- lib_service/src/main/res/values/strings.xml | 7 + .../android/service}/ExampleUnitTest.kt | 2 +- lib_utils/build.gradle | 2 + .../android/utils}/ExampleInstrumentedTest.kt | 4 +- .../src/main/AndroidManifest.xml | 2 +- .../java/no/nordicsemi/android/utils/Ext.kt | 16 + .../android/utils}/ExampleUnitTest.kt | 2 +- settings.gradle | 11 +- 52 files changed, 943 insertions(+), 964 deletions(-) rename lib_events/src/main/java/no/nordicsemi/android/events/CSCServiceData.kt => feature_csc/src/main/java/no/nordicsemi/android/csc/events/CSCServiceEvent.kt (84%) delete mode 100644 feature_csc/src/main/java/no/nordicsemi/android/csc/service/BleProfileService.java rename lib_broadcast/src/main/java/no/nordicsemi/android/broadcast/BluetoothDataReadBroadcast.kt => feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCDataReadBroadcast.kt (50%) rename feature_csc/src/main/java/no/nordicsemi/android/csc/{batery => service}/CSCMeasurementParser.kt (98%) create mode 100644 feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt rename feature_csc/src/main/java/no/nordicsemi/android/csc/{view => viewmodel}/CscViewModel.kt (64%) rename {lib_scanner => feature_scanner}/build.gradle (89%) rename {lib_scanner => feature_scanner}/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt (100%) rename {lib_scanner => feature_scanner}/src/main/AndroidManifest.xml (100%) create mode 100644 feature_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt rename {lib_scanner => feature_scanner}/src/main/java/no/nordicsemi/android/scanner/ScannerNavigation.kt (72%) rename {lib_scanner => feature_scanner}/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt (50%) rename {lib_scanner => feature_scanner}/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt (74%) create mode 100644 feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/SelectedBluetoothDeviceHolder.kt rename {lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui => feature_scanner/src/main/java/no/nordicsemi/android/scanner/view}/BluetoothNotAvailableScreen.kt (96%) rename {lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui => feature_scanner/src/main/java/no/nordicsemi/android/scanner/view}/RequestPermissionScreen.kt (89%) rename {lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui => feature_scanner/src/main/java/no/nordicsemi/android/scanner/view}/ScanDeviceScreen.kt (74%) rename {lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui => feature_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel}/NordicBleScannerViewModel.kt (59%) rename {lib_scanner => feature_scanner}/src/main/res/values/strings.xml (100%) rename {lib_scanner => feature_scanner}/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt (100%) delete mode 100644 lib_broadcast/build.gradle delete mode 100644 lib_broadcast/src/main/AndroidManifest.xml delete mode 100644 lib_events/build.gradle delete mode 100644 lib_events/src/main/java/no/nordicsemi/android/events/BluetoothReadDataEvent.kt delete mode 100644 lib_events/src/main/java/no/nordicsemi/android/events/Ext.kt delete mode 100644 lib_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt create mode 100644 lib_service/build.gradle rename {lib_broadcast/src/androidTest/java/no/nordicsemi/android/broadcast => lib_service/src/androidTest/java/no/nordicsemi/android/service}/ExampleInstrumentedTest.kt (81%) create mode 100644 lib_service/src/main/AndroidManifest.xml rename {feature_csc/src/main/java/no/nordicsemi/android/csc/batery => lib_service/src/main/java/no/nordicsemi/android/service}/BatteryManager.kt (96%) rename {feature_csc/src/main/java/no/nordicsemi/android/csc => lib_service/src/main/java/no/nordicsemi/android}/service/BatteryManagerCallbacks.kt (83%) create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/BluetoothDataReadBroadcast.kt create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt rename {feature_csc/src/main/java/no/nordicsemi/android/csc/batery => lib_service/src/main/java/no/nordicsemi/android/service}/LoggableBleManager.kt (73%) create mode 100644 lib_service/src/main/res/values/strings.xml rename {lib_broadcast/src/test/java/no/nordicsemi/android/broadcast => lib_service/src/test/java/no/nordicsemi/android/service}/ExampleUnitTest.kt (88%) create mode 100644 lib_utils/build.gradle rename {lib_events/src/androidTest/java/no/nordicsemi/android/events => lib_utils/src/androidTest/java/no/nordicsemi/android/utils}/ExampleInstrumentedTest.kt (82%) rename {lib_events => lib_utils}/src/main/AndroidManifest.xml (73%) create mode 100644 lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt rename {lib_events/src/test/java/no/nordicsemi/android/events => lib_utils/src/test/java/no/nordicsemi/android/utils}/ExampleUnitTest.kt (89%) diff --git a/app/build.gradle b/app/build.gradle index 0f88e4f5..ac2b5294 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,10 +51,8 @@ dependencies { //Hilt requires to implement every module in the main app module //https://github.com/google/dagger/issues/2123 implementation project(":feature_csc") - implementation project(":lib_broadcast") - implementation project(":lib_events") implementation project(":lib_theme") - implementation project(":lib_scanner") + implementation project(':feature_scanner') implementation libs.nordic.ble.common diff --git a/build.gradle b/build.gradle index 653a5100..0f5de76c 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { compose_version = '1.1.0-alpha03' kotlin_version = '1.5.30' android_api_version = 31 - android_min_api_version = 21 + android_min_api_version = 26 } repositories { google() diff --git a/feature_csc/build.gradle b/feature_csc/build.gradle index 7d0c8cb5..0b49fa39 100644 --- a/feature_csc/build.gradle +++ b/feature_csc/build.gradle @@ -2,10 +2,10 @@ apply from: rootProject.file("library.gradle") apply plugin: 'kotlin-parcelize' dependencies { - implementation project(":lib_broadcast") - implementation project(":lib_events") + implementation project(":lib_service") implementation project(":lib_theme") - implementation project(":lib_scanner") + implementation project(':feature_scanner') + implementation project(":lib_utils") implementation libs.nordic.ble.common diff --git a/lib_events/src/main/java/no/nordicsemi/android/events/CSCServiceData.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/events/CSCServiceEvent.kt similarity index 84% rename from lib_events/src/main/java/no/nordicsemi/android/events/CSCServiceData.kt rename to feature_csc/src/main/java/no/nordicsemi/android/csc/events/CSCServiceEvent.kt index 9b20424e..64f7de80 100644 --- a/lib_events/src/main/java/no/nordicsemi/android/events/CSCServiceData.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/events/CSCServiceEvent.kt @@ -1,10 +1,10 @@ -package no.nordicsemi.android.events +package no.nordicsemi.android.csc.events import android.bluetooth.BluetoothDevice import android.os.Parcelable import kotlinx.parcelize.Parcelize -sealed class CSCServiceEvent : BluetoothReadDataEvent(), Parcelable +sealed class CSCServiceEvent : Parcelable @Parcelize data class OnDistanceChangedEvent( diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/BleProfileService.java b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/BleProfileService.java deleted file mode 100644 index d331cd0a..00000000 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/BleProfileService.java +++ /dev/null @@ -1,613 +0,0 @@ -/* - * Copyright (c) 2015, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package no.nordicsemi.android.csc.service; - -import android.app.Service; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.Uri; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import android.util.Log; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.lifecycle.LifecycleService; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import no.nordicsemi.android.ble.BleManager; -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.ble.utils.ILogger; -import no.nordicsemi.android.csc.R; -import no.nordicsemi.android.csc.batery.LoggableBleManager; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.Logger; - -@SuppressWarnings("unused") -public abstract class BleProfileService extends LifecycleService implements BleManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "BleProfileService"; - - public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE"; - public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED"; - public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY"; - public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE"; - @Deprecated - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR"; - - /** - * The parameter passed when creating the service. Must contain the address of the sensor that we want to connect to - */ - public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS"; - /** - * The key for the device name that is returned in {@link #BROADCAST_CONNECTION_STATE} with state {@link #STATE_CONNECTED}. - */ - public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME"; - public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"; - public static final String EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI"; - public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE"; - public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE"; - public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY"; - public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY"; - @Deprecated - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE"; - public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE"; - - public static final int STATE_LINK_LOSS = -1; - public static final int STATE_DISCONNECTED = 0; - public static final int STATE_CONNECTED = 1; - public static final int STATE_CONNECTING = 2; - public static final int STATE_DISCONNECTING = 3; - - private LoggableBleManager bleManager; - private Handler handler; - - protected boolean bound; - private boolean activityIsChangingConfiguration; - private BluetoothDevice bluetoothDevice; - private String deviceName; - private ILogSession logSession; - - private final BroadcastReceiver bluetoothStateBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); - final ILogger logger = getBinder(); - - final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(state); - logger.log(Log.DEBUG, stateString); - - switch (state) { - case BluetoothAdapter.STATE_ON: - onBluetoothEnabled(); - break; - case BluetoothAdapter.STATE_TURNING_OFF: - case BluetoothAdapter.STATE_OFF: - onBluetoothDisabled(); - break; - } - } - - private String state2String(final int state) { - switch (state) { - case BluetoothAdapter.STATE_TURNING_ON: - return "TURNING ON"; - case BluetoothAdapter.STATE_ON: - return "ON"; - case BluetoothAdapter.STATE_TURNING_OFF: - return "TURNING OFF"; - case BluetoothAdapter.STATE_OFF: - return "OFF"; - default: - return "UNKNOWN (" + state + ")"; - } - } - }; - - public class LocalBinder extends Binder implements ILogger { - /** - * Disconnects from the sensor. - */ - public final void disconnect() { - final int state = bleManager.getConnectionState(); - if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) { - bleManager.close(); - onDeviceDisconnected(bluetoothDevice); - return; - } - - bleManager.disconnect().enqueue(); - } - - /** - * Sets whether the bound activity if changing configuration or not. - * If false, we will turn off battery level notifications in onUnbind(..) method below. - * - * @param changing true if the bound activity is finishing - */ - public void setActivityIsChangingConfiguration(final boolean changing) { - activityIsChangingConfiguration = changing; - } - - /** - * Returns the device address - * - * @return device address - */ - public String getDeviceAddress() { - return bluetoothDevice.getAddress(); - } - - /** - * Returns the device name - * - * @return the device name - */ - public String getDeviceName() { - return deviceName; - } - - /** - * Returns the Bluetooth device - * - * @return the Bluetooth device - */ - public BluetoothDevice getBluetoothDevice() { - return bluetoothDevice; - } - - /** - * Returns true if the device is connected to the sensor. - * - * @return true if device is connected to the sensor, false otherwise - */ - public boolean isConnected() { - return bleManager.isConnected(); - } - - - /** - * Returns the connection state of given device. - * - * @return the connection state, as in {@link BleManager#getConnectionState()}. - */ - public int getConnectionState() { - return bleManager.getConnectionState(); - } - - /** - * Returns the log session that can be used to append log entries. - * The log session is created when the service is being created. - * The method returns null if the nRF Logger app was not installed. - * - * @return the log session - */ - public ILogSession getLogSession() { - return logSession; - } - - @Override - public void log(final int level, @NonNull final String message) { - Logger.log(logSession, level, message); - } - - @Override - public void log(final int level, final @StringRes int messageRes, final Object... params) { - Logger.log(logSession, level, messageRes, params); - } - } - - /** - * Returns a handler that is created in onCreate(). - * The handler may be used to postpone execution of some operations or to run them in UI thread. - */ - protected Handler getHandler() { - return handler; - } - - /** - * Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity. - * - * @return the service binder - */ - protected LocalBinder getBinder() { - // default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation - return new LocalBinder(); - } - - @Override - public IBinder onBind(final Intent intent) { - bound = true; - return getBinder(); - } - - @Override - public final void onRebind(final Intent intent) { - bound = true; - - if (!activityIsChangingConfiguration) - onRebind(); - } - - /** - * Called when the activity has rebound to the service after being recreated. - * This method is not called when the activity was killed to be recreated when the phone orientation changed - * if prior to being killed called {@link LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true. - */ - protected void onRebind() { - // empty default implementation - } - - @Override - public final boolean onUnbind(final Intent intent) { - bound = false; - - if (!activityIsChangingConfiguration) - onUnbind(); - - // We want the onRebind method be called if anything else binds to it again - return true; - } - - /** - * Called when the activity has unbound from the service before being finished. - * This method is not called when the activity is killed to be recreated when the phone orientation changed. - */ - protected void onUnbind() { - // empty default implementation - } - - @SuppressWarnings("unchecked") - @Override - public void onCreate() { - super.onCreate(); - - handler = new Handler(); - - // Initialize the manager - bleManager = initializeManager(); - bleManager.setGattCallbacks(this); - - // Register broadcast receivers - registerReceiver(bluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - - // Service has now been created - onServiceCreated(); - - // Call onBluetoothEnabled if Bluetooth enabled - final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (bluetoothAdapter.isEnabled()) { - onBluetoothEnabled(); - } - } - - /** - * Called when the service has been created, before the {@link #onBluetoothEnabled()} is called. - */ - protected void onServiceCreated() { - // empty default implementation - } - - /** - * Initializes the Ble Manager responsible for connecting to a single device. - * - * @return a new BleManager object - */ - @SuppressWarnings("rawtypes") - protected abstract LoggableBleManager initializeManager(); - - /** - * This method returns whether autoConnect option should be used. - * - * @return true to use autoConnect feature, false (default) otherwise. - */ - protected boolean shouldAutoConnect() { - return false; - } - - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS)) - throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key"); - - final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI); - logSession = Logger.openSession(getApplicationContext(), logUri); - deviceName = intent.getStringExtra(EXTRA_DEVICE_NAME); - - Logger.i(logSession, "Service started"); - - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); - bluetoothDevice = adapter.getRemoteDevice(deviceAddress); - - bleManager.setLogger(logSession); - onServiceStarted(); - bleManager.connect(bluetoothDevice) - .useAutoConnect(shouldAutoConnect()) - .retry(3, 100) - .enqueue(); - return START_REDELIVER_INTENT; - } - - /** - * Called when the service has been started. The device name and address are set. - * The BLE Manager will try to connect to the device after this method finishes. - */ - protected void onServiceStarted() { - // empty default implementation - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - // This method is called when user removed the app from Recents. - // By default, the service will be killed and recreated immediately after that. - // However, all managed devices will be lost and devices will be disconnected. - stopSelf(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - // Unregister broadcast receivers - unregisterReceiver(bluetoothStateBroadcastReceiver); - - // shutdown the manager - bleManager.close(); - Logger.i(logSession, "Service destroyed"); - bleManager = null; - bluetoothDevice = null; - deviceName = null; - logSession = null; - handler = null; - } - - /** - * Method called when Bluetooth Adapter has been disabled. - */ - protected void onBluetoothDisabled() { - // empty default implementation - } - - /** - * This method is called when Bluetooth Adapter has been enabled and - * after the service was created if Bluetooth Adapter was enabled at that moment. - * This method could initialize all Bluetooth related features, for example open the GATT server. - */ - protected void onBluetoothEnabled() { - // empty default implementation - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_DEVICE_NAME, deviceName); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - // Notify user about changing the state to DISCONNECTING - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * This method should return false if the service needs to do some asynchronous work after if has disconnected from the device. - * In that case the {@link #stopService()} method must be called when done. - * - * @return true (default) to automatically stop the service when device is disconnected. False otherwise. - */ - protected boolean stopWhenDisconnected() { - return true; - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - // Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above - - // Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be - // invoked ONLY when user requested disconnection (using Disconnect button). If the device - // disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead. - - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - if (stopWhenDisconnected()) - stopService(); - } - - protected void stopService() { - // user requested disconnection. We must stop the service - Logger.v(logSession, "Stopping service..."); - stopSelf(); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_DEVICE_READY); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - // no need for disconnecting, it will be disconnected by the manager automatically - } - - @Override - public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - showToast(R.string.csc_bonding); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - showToast(R.string.csc_bonded); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - showToast(R.string.csc_bonding_failed); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - final Intent broadcast = new Intent(BROADCAST_ERROR); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_ERROR_MESSAGE, message); - broadcast.putExtra(EXTRA_ERROR_CODE, errorCode); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - handler.post(() -> Toast.makeText(BleProfileService.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message a message to be shown - */ - protected void showToast(final String message) { - handler.post(() -> Toast.makeText(BleProfileService.this, message, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns the log session that can be used to append log entries. The method returns null if the nRF Logger app was not installed. It is safe to use logger when - * {@link #onServiceStarted()} has been called. - * - * @return the log session - */ - protected ILogSession getLogSession() { - return logSession; - } - - /** - * Returns the device address - * - * @return device address - */ - protected String getDeviceAddress() { - return bluetoothDevice.getAddress(); - } - - /** - * Returns the Bluetooth device object - * - * @return bluetooth device - */ - protected BluetoothDevice getBluetoothDevice() { - return bluetoothDevice; - } - - /** - * Returns the device name - * - * @return the device name - */ - protected String getDeviceName() { - return deviceName; - } - - /** - * Returns true if the device is connected to the sensor. - * - * @return true if device is connected to the sensor, false otherwise - */ - protected boolean isConnected() { - return bleManager != null && bleManager.isConnected(); - } -} diff --git a/lib_broadcast/src/main/java/no/nordicsemi/android/broadcast/BluetoothDataReadBroadcast.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCDataReadBroadcast.kt similarity index 50% rename from lib_broadcast/src/main/java/no/nordicsemi/android/broadcast/BluetoothDataReadBroadcast.kt rename to feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCDataReadBroadcast.kt index 188e25cb..930d1d8c 100644 --- a/lib_broadcast/src/main/java/no/nordicsemi/android/broadcast/BluetoothDataReadBroadcast.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCDataReadBroadcast.kt @@ -1,21 +1,15 @@ -package no.nordicsemi.android.broadcast +package no.nordicsemi.android.csc.service import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import no.nordicsemi.android.events.BluetoothReadDataEvent +import no.nordicsemi.android.csc.events.CSCServiceEvent +import no.nordicsemi.android.service.BluetoothDataReadBroadcast import javax.inject.Inject import javax.inject.Singleton @Singleton -class BluetoothDataReadBroadcast @Inject constructor() { - - private val _event = MutableSharedFlow( - replay = 1, - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val events: SharedFlow = _event +class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast() { private val _wheelSize = MutableSharedFlow( replay = 1, @@ -24,10 +18,6 @@ class BluetoothDataReadBroadcast @Inject constructor() { ) val wheelSize: SharedFlow = _wheelSize - fun offer(newEvent: BluetoothReadDataEvent) { - _event.tryEmit(newEvent) - } - fun setWheelSize(size: Int) { _wheelSize.tryEmit(size) } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt index 2d9ae285..22a11caa 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt @@ -29,17 +29,16 @@ import android.util.Log import androidx.annotation.FloatRange import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback import no.nordicsemi.android.ble.data.Data -import no.nordicsemi.android.csc.batery.BatteryManager -import no.nordicsemi.android.csc.batery.CSCMeasurementParser.parse +import no.nordicsemi.android.csc.service.CSCMeasurementParser.parse +import no.nordicsemi.android.csc.view.CSCSettings import no.nordicsemi.android.log.LogContract +import no.nordicsemi.android.service.BatteryManager import java.util.* -private const val SETTINGS_WHEEL_SIZE_DEFAULT = 2340 - -internal class CSCManager(context: Context) : BatteryManager(context) { +internal class CSCManager(context: Context) : BatteryManager(context) { private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null - private var wheelSize = SETTINGS_WHEEL_SIZE_DEFAULT + private var wheelSize = CSCSettings.DefaultWheelSize.VALUE override fun getGattCallback(): BatteryManagerGattCallback { return CSCManagerGattCallback() @@ -77,7 +76,7 @@ internal class CSCManager(context: Context) : BatteryManager(), CSCManagerCallbacks { - @Inject lateinit var localBroadcast: BluetoothDataReadBroadcast + @Inject + lateinit var localBroadcast: CSCDataReadBroadcast - override fun initializeManager(): LoggableBleManager { - return CSCManager(this).also { manager = it } + override val manager: CSCManager by lazy { + CSCManager(this).apply { + setGattCallbacks(this@CSCService) + } + } + + override fun initializeManager(): LoggableBleManager { + return manager } override fun onCreate() { super.onCreate() - val filter = IntentFilter() - filter.addAction(ACTION_DISCONNECT) - registerReceiver(disconnectActionBroadcastReceiver, filter) localBroadcast.wheelSize.onEach { - manager?.setWheelSize(it) - }.launchIn(GlobalScope) - } - - override fun onDestroy() { - // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - cancelNotification() - unregisterReceiver(disconnectActionBroadcastReceiver) - super.onDestroy() - } - - override fun onRebind() { - stopForegroundService() - if (isConnected) { - // This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property). - // If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications. - manager!!.readBatteryLevelCharacteristic() - } - } - - override fun onUnbind() { - // When we are connected, but the application is not open, we are not really interested in battery level notifications. - // But we will still be receiving other values, if enabled. - if (isConnected) manager!!.disableBatteryLevelCharacteristicNotifications() - startForegroundService() + manager.setWheelSize(it) + }.launchIn(lifecycleScope) } override fun onDistanceChanged( @@ -106,84 +56,4 @@ internal class CSCService : BleProfileService(), CSCManagerCallbacks { override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) { localBroadcast.offer(OnBatteryLevelChanged(bluetoothDevice, batteryLevel)) } - - /** - * Sets the service as a foreground service - */ - private fun startForegroundService() { - // when the activity closes we need to show the notification that user is connected to the peripheral sensor - // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services - val notification = createNotification(R.string.csc_notification_connected_message, 0) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification) - } else { - val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - nm.notify(NOTIFICATION_ID, notification) - } - } - - /** - * Stops the service as a foreground service - */ - private fun stopForegroundService() { - // when the activity rebinds to the service, remove the notification and stop the foreground service - // on devices running Android 8.0 (Oreo) or above - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - stopForeground(true) - } else { - cancelNotification() - } - } - - /** - * Creates the notification - * - * @param messageResId the message resource id. The message must have one String parameter,

- * f.e. `%s is connected` - * @param defaults - */ - private fun createNotification(messageResId: Int, defaults: Int): Notification { - TODO() -// final Intent parentIntent = new Intent(this, FeaturesActivity.class); -// parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); -// final Intent targetIntent = new Intent(this, CSCActivity.class); -// -// final Intent disconnect = new Intent(ACTION_DISCONNECT); -// final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); -// -// // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed -// final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); -// final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); -// builder.setContentIntent(pendingIntent); -// builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); -// builder.setSmallIcon(R.drawable.ic_stat_notify_csc); -// builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); -// builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction)); -// -// return builder.build(); - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private fun cancelNotification() { - val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - nm.cancel(NOTIFICATION_ID) - } - - /** - * This broadcast receiver listens for [.ACTION_DISCONNECT] that may be fired by pressing Disconnect action button on the notification. - */ - private val disconnectActionBroadcastReceiver: BroadcastReceiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - Logger.i(logSession, "[Notification] Disconnect action pressed") - if (isConnected) binder.disconnect() else stopSelf() - } - } - - companion object { - private const val ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.csc.ACTION_DISCONNECT" - private const val NOTIFICATION_ID = 200 - } -} +} \ No newline at end of file diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt new file mode 100644 index 00000000..e38cdc12 --- /dev/null +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt @@ -0,0 +1,9 @@ +package no.nordicsemi.android.csc.view + +object CSCSettings { + + object DefaultWheelSize { + const val NAME = "60-622" + const val VALUE = 2340 + } +} \ No newline at end of file diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewConnectedState.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewConnectedState.kt index dc0a8665..6af8f324 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewConnectedState.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewConnectedState.kt @@ -1,7 +1,5 @@ package no.nordicsemi.android.csc.view -import no.nordicsemi.android.events.EMPTY - internal sealed class CSCViewState { fun ensureConnectedState(): CSCViewConnectedState { @@ -30,7 +28,7 @@ internal data class CSCViewConnectedState( val totalDistance: Float = 0f, val gearRatio: Float = 0f, val batteryLevel: Int = 0, - val wheelSize: String = String.EMPTY + val wheelSize: String = CSCSettings.DefaultWheelSize.NAME ) : CSCViewState() { fun displaySpeed(): String { diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt index 40784d36..3e5311b7 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt @@ -25,7 +25,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.service.CSCService -import no.nordicsemi.android.events.exhaustive +import no.nordicsemi.android.csc.viewmodel.CscViewModel +import no.nordicsemi.android.utils.exhaustive +import no.nordicsemi.android.utils.isServiceRunning @Composable internal fun CscScreen(navController: NavController, viewModel: CscViewModel = hiltViewModel()) { @@ -37,11 +39,6 @@ internal fun CscScreen(navController: NavController, viewModel: CscViewModel = h secondScreenResult?.value?.let { viewModel.onEvent(OnBluetoothDeviceSelected(it)) - val intent = Intent(LocalContext.current, CSCService::class.java).apply { - putExtra("no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS", it.address) - } - LocalContext.current.startService(intent) - navController.currentBackStackEntry ?.savedStateHandle ?.set("result", null) @@ -77,7 +74,14 @@ private fun NotConnectedScreen( onEvent(OnMovedToScannerScreen) } + if (LocalContext.current.isServiceRunning(CSCService::class.java.name)) { + val intent = Intent(LocalContext.current, CSCService::class.java) + LocalContext.current.stopService(intent) + } + NotConnectedView(onEvent) + + LocalContext.current.stopService(Intent(LocalContext.current, CSCService::class.java)) } @Composable @@ -105,6 +109,11 @@ private fun ConnectedView(state: CSCViewConnectedState, onEvent: (CSCViewEvent) SelectWheelSizeDialog { onEvent(it) } } + if (!LocalContext.current.isServiceRunning(CSCService::class.java.name)) { + val intent = Intent(LocalContext.current, CSCService::class.java) + LocalContext.current.startService(intent) + } + Column( modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -118,7 +127,6 @@ private fun ConnectedView(state: CSCViewConnectedState, onEvent: (CSCViewEvent) Button(onClick = { onEvent(OnDisconnectButtonClick) }) { Text(text = stringResource(id = R.string.csc_disconnect)) } - } } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscViewModel.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CscViewModel.kt similarity index 64% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscViewModel.kt rename to feature_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CscViewModel.kt index 61ba18a4..b2cde153 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscViewModel.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CscViewModel.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.csc.view +package no.nordicsemi.android.csc.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -8,27 +8,44 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext -import no.nordicsemi.android.broadcast.BluetoothDataReadBroadcast -import no.nordicsemi.android.events.CSCServiceEvent -import no.nordicsemi.android.events.CrankDataChanged -import no.nordicsemi.android.events.OnBatteryLevelChanged -import no.nordicsemi.android.events.OnDistanceChangedEvent -import no.nordicsemi.android.events.exhaustive +import no.nordicsemi.android.csc.events.CSCServiceEvent +import no.nordicsemi.android.csc.events.CrankDataChanged +import no.nordicsemi.android.csc.events.OnBatteryLevelChanged +import no.nordicsemi.android.csc.events.OnDistanceChangedEvent +import no.nordicsemi.android.csc.service.CSCDataReadBroadcast +import no.nordicsemi.android.csc.view.CSCViewConnectedState +import no.nordicsemi.android.csc.view.CSCViewEvent +import no.nordicsemi.android.csc.view.CSCViewNotConnectedState +import no.nordicsemi.android.csc.view.CSCViewState +import no.nordicsemi.android.csc.view.OnBluetoothDeviceSelected +import no.nordicsemi.android.csc.view.OnConnectButtonClick +import no.nordicsemi.android.csc.view.OnDisconnectButtonClick +import no.nordicsemi.android.csc.view.OnMovedToScannerScreen +import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected +import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick +import no.nordicsemi.android.csc.view.OnWheelSizeSelected +import no.nordicsemi.android.scanner.tools.SelectedBluetoothDeviceHolder +import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @HiltViewModel internal class CscViewModel @Inject constructor( - private val localBroadcast: BluetoothDataReadBroadcast + private val localBroadcast: CSCDataReadBroadcast, + private val deviceHolder: SelectedBluetoothDeviceHolder ) : ViewModel() { - val state = MutableStateFlow(CSCViewNotConnectedState()) + val state = MutableStateFlow(createInitialState()) init { localBroadcast.events.onEach { - (it as? CSCServiceEvent)?.let { withContext(Dispatchers.Main) { consumeEvent(it) }} + withContext(Dispatchers.Main) { consumeEvent(it) } }.launchIn(viewModelScope) } + private fun createInitialState(): CSCViewState { + return deviceHolder.device?.let { CSCViewConnectedState() } ?: CSCViewNotConnectedState() + } + private fun consumeEvent(event: CSCServiceEvent) { val newValue = when (event) { is CrankDataChanged -> createNewState(event) @@ -64,7 +81,7 @@ internal class CscViewModel @Inject constructor( is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event) OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent() is OnWheelSizeSelected -> onWheelSizeChanged(event) - OnDisconnectButtonClick -> TODO() + OnDisconnectButtonClick -> onDisconnectButtonClick() OnConnectButtonClick -> onConnectButtonClick() OnMovedToScannerScreen -> onOnMovedToScannerScreen() is OnBluetoothDeviceSelected -> onBluetoothDeviceSelected() @@ -87,6 +104,10 @@ internal class CscViewModel @Inject constructor( )) } + private fun onDisconnectButtonClick() { + state.tryEmit(CSCViewNotConnectedState()) + } + private fun onConnectButtonClick() { state.tryEmit(state.value.ensureDisconnectedState().copy(showScannerDialog = true)) } diff --git a/feature_csc/src/main/res/values/strings.xml b/feature_csc/src/main/res/values/strings.xml index 4e00e439..9a0b11a9 100644 --- a/feature_csc/src/main/res/values/strings.xml +++ b/feature_csc/src/main/res/values/strings.xml @@ -2,16 +2,10 @@ Cyclic and speed cadence - Bonding with the device… - The device is now bonded. - Bonding failed. - Disconnect No device connected Connect - %s is connected. - Speed Cadence Distance diff --git a/lib_scanner/build.gradle b/feature_scanner/build.gradle similarity index 89% rename from lib_scanner/build.gradle rename to feature_scanner/build.gradle index ee5c7337..b29a2804 100644 --- a/lib_scanner/build.gradle +++ b/feature_scanner/build.gradle @@ -2,8 +2,8 @@ apply from: rootProject.file("library.gradle") apply plugin: 'kotlin-parcelize' dependencies { + implementation project(":lib_utils") implementation project(":lib_theme") - implementation project(":lib_events") implementation libs.material implementation libs.google.permissions diff --git a/lib_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt b/feature_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt similarity index 100% rename from lib_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt rename to feature_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt diff --git a/lib_scanner/src/main/AndroidManifest.xml b/feature_scanner/src/main/AndroidManifest.xml similarity index 100% rename from lib_scanner/src/main/AndroidManifest.xml rename to feature_scanner/src/main/AndroidManifest.xml diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt new file mode 100644 index 00000000..6b906bd9 --- /dev/null +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt @@ -0,0 +1,30 @@ +package no.nordicsemi.android.scanner + +import android.bluetooth.BluetoothAdapter +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.scanner.tools.SelectedBluetoothDeviceHolder +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object HiltModule { + + @Provides + fun createNordicBleScanner(): BluetoothAdapter? { + return BluetoothAdapter.getDefaultAdapter() + } + + @Singleton + @Provides + fun createSelectedBluetoothDeviceHolder( + @ApplicationContext context: Context, + bluetoothAdapter: BluetoothAdapter? + ): SelectedBluetoothDeviceHolder { + return SelectedBluetoothDeviceHolder(context, bluetoothAdapter) + } +} diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ScannerNavigation.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/ScannerNavigation.kt similarity index 72% rename from lib_scanner/src/main/java/no/nordicsemi/android/scanner/ScannerNavigation.kt rename to feature_scanner/src/main/java/no/nordicsemi/android/scanner/ScannerNavigation.kt index c4f29664..eecf29a4 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ScannerNavigation.kt +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/ScannerNavigation.kt @@ -8,20 +8,17 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController -import no.nordicsemi.android.events.exhaustive import no.nordicsemi.android.scanner.tools.ScannerStatus -import no.nordicsemi.android.scanner.ui.BluetoothNotAvailableScreen -import no.nordicsemi.android.scanner.ui.BluetoothNotEnabledScreen -import no.nordicsemi.android.scanner.ui.NordicBleScannerViewModel -import no.nordicsemi.android.scanner.ui.RequestPermissionScreen -import no.nordicsemi.android.scanner.ui.ScanDeviceScreen -import no.nordicsemi.android.scanner.ui.ScannerViewEvent +import no.nordicsemi.android.scanner.view.* +import no.nordicsemi.android.scanner.viewmodel.NordicBleScannerViewModel +import no.nordicsemi.android.scanner.viewmodel.ScannerViewEvent +import no.nordicsemi.android.utils.exhaustive @Composable fun ScannerRoute(navController: NavController) { val viewModel = hiltViewModel() - val scannerStatus = viewModel.state.collectAsState().value.scannerStatus + val scannerStatus = viewModel.state.collectAsState().value Column { TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__devices_list)) }) diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt similarity index 50% rename from lib_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt rename to feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt index 4e9dd055..4ec1ad37 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt @@ -3,8 +3,6 @@ package no.nordicsemi.android.scanner.tools import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice -import android.bluetooth.le.ScanCallback -import android.bluetooth.le.ScanResult import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @@ -13,22 +11,6 @@ internal class NordicBleScanner @Inject constructor(private val bleAdapter: Blue val scannerResult = MutableStateFlow(DeviceListResult()) - private var isScanning = false - - private val scanner by lazy { bleAdapter?.bluetoothLeScanner } - private val devices = mutableListOf() - - private val scanningCallback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, result: ScanResult?) { - result?.device?.let { devices.addIfNotExist(it) } - scannerResult.value = DeviceListResult(devices) - } - - override fun onScanFailed(errorCode: Int) { - scannerResult.value = ScanningErrorResult - } - } - fun getBluetoothStatus(): ScannerStatus { return when { bleAdapter == null -> ScannerStatus.NOT_AVAILABLE @@ -36,22 +18,6 @@ internal class NordicBleScanner @Inject constructor(private val bleAdapter: Blue else -> ScannerStatus.DISABLED } } - - fun startScanning() { - if (isScanning) { - return - } - isScanning = true - scanner?.startScan(scanningCallback) - } - - fun stopScanning() { - if (!isScanning) { - return - } - isScanning = false - scanner?.stopScan(scanningCallback) - } } sealed class ScanningResult diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt similarity index 74% rename from lib_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt rename to feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt index 5f992fe5..e08da090 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt @@ -1,5 +1,5 @@ package no.nordicsemi.android.scanner.tools -enum class ScannerStatus { +internal enum class ScannerStatus { PERMISSION_REQUIRED, ENABLED, DISABLED, NOT_AVAILABLE } diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/SelectedBluetoothDeviceHolder.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/SelectedBluetoothDeviceHolder.kt new file mode 100644 index 00000000..41284baa --- /dev/null +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/SelectedBluetoothDeviceHolder.kt @@ -0,0 +1,25 @@ +package no.nordicsemi.android.scanner.tools + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.companion.CompanionDeviceManager +import android.content.Context + +class SelectedBluetoothDeviceHolder constructor( + private val context: Context, + private val bluetoothAdapter: BluetoothAdapter? +) { + + val device: BluetoothDevice? + get() { + val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager + return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) } + } + + fun forgetDevice() { + device?.let { + val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager + deviceManager.disassociate(it.address) + } + } +} diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/BluetoothNotAvailableScreen.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt similarity index 96% rename from lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/BluetoothNotAvailableScreen.kt rename to feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt index 5594d858..503bcc97 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/BluetoothNotAvailableScreen.kt +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.scanner.ui +package no.nordicsemi.android.scanner.view import android.app.Activity import android.bluetooth.BluetoothAdapter diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/RequestPermissionScreen.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt similarity index 89% rename from lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/RequestPermissionScreen.kt rename to feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt index 6e7d5918..75da5753 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/RequestPermissionScreen.kt +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt @@ -1,17 +1,10 @@ -package no.nordicsemi.android.scanner.ui +package no.nordicsemi.android.scanner.view import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/ScanDeviceScreen.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt similarity index 74% rename from lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/ScanDeviceScreen.kt rename to feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt index aef0b6b2..4ddc7084 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/ScanDeviceScreen.kt +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt @@ -1,9 +1,10 @@ -package no.nordicsemi.android.scanner.ui +package no.nordicsemi.android.scanner.view import android.app.Activity import android.bluetooth.BluetoothDevice +import android.bluetooth.le.ScanResult import android.companion.AssociationRequest -import android.companion.BluetoothDeviceFilter +import android.companion.BluetoothLeDeviceFilter import android.companion.CompanionDeviceManager import android.content.Context import android.content.IntentSender @@ -20,10 +21,11 @@ import androidx.navigation.NavController @Composable fun ScanDeviceScreen(navController: NavController,) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder() + val deviceFilter = BluetoothLeDeviceFilter.Builder() .build() val pairingRequest: AssociationRequest = AssociationRequest.Builder() + .addDeviceFilter(deviceFilter) .build() val deviceManager = @@ -32,13 +34,18 @@ fun ScanDeviceScreen(navController: NavController,) { val contract = ActivityResultContracts.StartIntentSenderForResult() val launcher = rememberLauncherForActivityResult(contract = contract, onResult = { if (it.resultCode == Activity.RESULT_OK) { - val deviceToPair: BluetoothDevice? = it.data?.getParcelableExtra( - CompanionDeviceManager.EXTRA_DEVICE) + //Sometimes result is ScanResult & sometimes BluetoothDevice + val device: BluetoothDevice = try { + it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)!! + } catch (e: Exception) { + (it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE))!!.device + } + navController.previousBackStackEntry ?.savedStateHandle - ?.set("result", deviceToPair) - navController.popBackStack() + ?.set("result", device) } + navController.popBackStack() }) val hasBeenInvoked = remember { mutableStateOf(false) } diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/NordicBleScannerViewModel.kt b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/NordicBleScannerViewModel.kt similarity index 59% rename from lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/NordicBleScannerViewModel.kt rename to feature_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/NordicBleScannerViewModel.kt index cf1ff187..49f9d6fa 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/ui/NordicBleScannerViewModel.kt +++ b/feature_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/NordicBleScannerViewModel.kt @@ -1,11 +1,11 @@ -package no.nordicsemi.android.scanner.ui +package no.nordicsemi.android.scanner.viewmodel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import no.nordicsemi.android.events.exhaustive import no.nordicsemi.android.scanner.tools.NordicBleScanner import no.nordicsemi.android.scanner.tools.ScannerStatus +import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @HiltViewModel @@ -14,9 +14,7 @@ internal class NordicBleScannerViewModel @Inject constructor( ) : ViewModel() { val state = - MutableStateFlow(NordicBleScannerState(scannerStatus = ScannerStatus.PERMISSION_REQUIRED)) - - val scannerResult = bleScanner.scannerResult + MutableStateFlow(ScannerStatus.PERMISSION_REQUIRED) fun onEvent(event: ScannerViewEvent) { when (event) { @@ -26,19 +24,14 @@ internal class NordicBleScannerViewModel @Inject constructor( } private fun onPermissionChecked() { - state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus()) + state.value = bleScanner.getBluetoothStatus() } private fun onBluetoothEnabled() { - state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus()) - bleScanner.startScanning() + state.value = bleScanner.getBluetoothStatus() } } -enum class ScannerViewEvent { +internal enum class ScannerViewEvent { PERMISSION_CHECKED, BLUETOOTH_ENABLED } - -internal data class NordicBleScannerState( - val scannerStatus: ScannerStatus -) diff --git a/lib_scanner/src/main/res/values/strings.xml b/feature_scanner/src/main/res/values/strings.xml similarity index 100% rename from lib_scanner/src/main/res/values/strings.xml rename to feature_scanner/src/main/res/values/strings.xml diff --git a/lib_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt b/feature_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt similarity index 100% rename from lib_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt rename to feature_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt diff --git a/lib_broadcast/build.gradle b/lib_broadcast/build.gradle deleted file mode 100644 index 63c30c41..00000000 --- a/lib_broadcast/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -apply from: rootProject.file("library.gradle") - -dependencies { - implementation project(":lib_events") - - implementation libs.kotlin.coroutines -} diff --git a/lib_broadcast/src/main/AndroidManifest.xml b/lib_broadcast/src/main/AndroidManifest.xml deleted file mode 100644 index fd7a67c5..00000000 --- a/lib_broadcast/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/lib_events/build.gradle b/lib_events/build.gradle deleted file mode 100644 index caca198d..00000000 --- a/lib_events/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -apply from: rootProject.file("library.gradle") -apply plugin: 'kotlin-kapt' -apply plugin: 'kotlin-parcelize' \ No newline at end of file diff --git a/lib_events/src/main/java/no/nordicsemi/android/events/BluetoothReadDataEvent.kt b/lib_events/src/main/java/no/nordicsemi/android/events/BluetoothReadDataEvent.kt deleted file mode 100644 index fdb12bdd..00000000 --- a/lib_events/src/main/java/no/nordicsemi/android/events/BluetoothReadDataEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package no.nordicsemi.android.events - -sealed class BluetoothReadDataEvent diff --git a/lib_events/src/main/java/no/nordicsemi/android/events/Ext.kt b/lib_events/src/main/java/no/nordicsemi/android/events/Ext.kt deleted file mode 100644 index 67d479ad..00000000 --- a/lib_events/src/main/java/no/nordicsemi/android/events/Ext.kt +++ /dev/null @@ -1,7 +0,0 @@ -package no.nordicsemi.android.events - -val T.exhaustive - get() = this - -val String.Companion.EMPTY - get() = "" diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt b/lib_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt deleted file mode 100644 index ba0cd1cb..00000000 --- a/lib_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package no.nordicsemi.android.scanner - -import android.bluetooth.BluetoothAdapter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal object HiltModule { - - @Provides - fun createNordicBleScanner(): BluetoothAdapter? { - return BluetoothAdapter.getDefaultAdapter() - } -} diff --git a/lib_service/build.gradle b/lib_service/build.gradle new file mode 100644 index 00000000..51af2af3 --- /dev/null +++ b/lib_service/build.gradle @@ -0,0 +1,18 @@ +apply from: rootProject.file("library.gradle") +apply plugin: 'kotlin-parcelize' + +dependencies { + implementation project(":feature_scanner") + + implementation libs.nordic.ble.common + implementation libs.nordic.log + + implementation libs.lifecycle.service + implementation libs.localbroadcastmanager + + testImplementation libs.test.junit + androidTestImplementation libs.android.test.junit + androidTestImplementation libs.android.test.espresso + androidTestImplementation libs.android.test.compose.ui + debugImplementation libs.android.test.compose.tooling +} diff --git a/lib_broadcast/src/androidTest/java/no/nordicsemi/android/broadcast/ExampleInstrumentedTest.kt b/lib_service/src/androidTest/java/no/nordicsemi/android/service/ExampleInstrumentedTest.kt similarity index 81% rename from lib_broadcast/src/androidTest/java/no/nordicsemi/android/broadcast/ExampleInstrumentedTest.kt rename to lib_service/src/androidTest/java/no/nordicsemi/android/service/ExampleInstrumentedTest.kt index 21ddcc31..f0343138 100644 --- a/lib_broadcast/src/androidTest/java/no/nordicsemi/android/broadcast/ExampleInstrumentedTest.kt +++ b/lib_service/src/androidTest/java/no/nordicsemi/android/service/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.broadcast +package no.nordicsemi.android.service import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("no.nordicsemi.android.broadcast.test", appContext.packageName) + assertEquals("no.nordicsemi.android.service.test", appContext.packageName) } } \ No newline at end of file diff --git a/lib_service/src/main/AndroidManifest.xml b/lib_service/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c2a57b13 --- /dev/null +++ b/lib_service/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/batery/BatteryManager.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManager.kt similarity index 96% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/batery/BatteryManager.kt rename to lib_service/src/main/java/no/nordicsemi/android/service/BatteryManager.kt index 997809b6..fb67f14e 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/batery/BatteryManager.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManager.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.csc.batery +package no.nordicsemi.android.service import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt @@ -9,7 +9,6 @@ import androidx.annotation.IntRange import no.nordicsemi.android.ble.callback.DataReceivedCallback import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback import no.nordicsemi.android.ble.data.Data -import no.nordicsemi.android.csc.service.BatteryManagerCallbacks import no.nordicsemi.android.log.LogContract import java.util.* @@ -38,7 +37,7 @@ abstract class BatteryManager(context: Context) : ) { log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%") this@BatteryManager.batteryLevel = batteryLevel - mCallbacks!!.onBatteryLevelChanged(device, batteryLevel) + mCallbacks?.onBatteryLevelChanged(device, batteryLevel) } override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) { diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/BatteryManagerCallbacks.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManagerCallbacks.kt similarity index 83% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/service/BatteryManagerCallbacks.kt rename to lib_service/src/main/java/no/nordicsemi/android/service/BatteryManagerCallbacks.kt index 74e8f46a..feb51fcd 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/BatteryManagerCallbacks.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManagerCallbacks.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.csc.service +package no.nordicsemi.android.service import no.nordicsemi.android.ble.BleManagerCallbacks import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt new file mode 100644 index 00000000..722b89bb --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt @@ -0,0 +1,554 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.service + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Binder +import android.os.Handler +import android.os.IBinder +import android.util.Log +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.lifecycle.LifecycleService +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import dagger.hilt.android.AndroidEntryPoint +import no.nordicsemi.android.ble.BleManagerCallbacks +import no.nordicsemi.android.ble.utils.ILogger +import no.nordicsemi.android.log.ILogSession +import no.nordicsemi.android.log.Logger +import no.nordicsemi.android.scanner.tools.SelectedBluetoothDeviceHolder +import javax.inject.Inject + +@AndroidEntryPoint +abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { + + private var bleManager: LoggableBleManager? = null + + @Inject + lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder + + /** + * Returns a handler that is created in onCreate(). + * The handler may be used to postpone execution of some operations or to run them in UI thread. + */ + protected var handler: Handler? = null + private set + protected var bound = false + private var activityIsChangingConfiguration = false + + /** + * Returns the Bluetooth device object + * + * @return bluetooth device + */ + protected val bluetoothDevice: BluetoothDevice by lazy { + bluetoothDeviceHolder.device ?: throw UnsupportedOperationException( + "No device address at EXTRA_DEVICE_ADDRESS key" + ) + } + + /** + * Returns the device name + * + * @return the device name + */ + protected var deviceName: String? = null + private set + + /** + * Returns the log session that can be used to append log entries. The method returns `null` if the nRF Logger app was not installed. It is safe to use logger when + * [.onServiceStarted] has been called. + * + * @return the log session + */ + protected var logSession: ILogSession? = null + private set + private val bluetoothStateBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF) + val logger: ILogger = binder + val stateString = + "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String( + state + ) + logger.log(Log.DEBUG, stateString) + when (state) { + BluetoothAdapter.STATE_ON -> onBluetoothEnabled() + BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> onBluetoothDisabled() + } + } + + private fun state2String(state: Int): String { + return when (state) { + BluetoothAdapter.STATE_TURNING_ON -> "TURNING ON" + BluetoothAdapter.STATE_ON -> "ON" + BluetoothAdapter.STATE_TURNING_OFF -> "TURNING OFF" + BluetoothAdapter.STATE_OFF -> "OFF" + else -> "UNKNOWN ($state)" + } + } + } + + inner class LocalBinder : Binder(), ILogger { + /** + * Disconnects from the sensor. + */ + fun disconnect() { + val state = bleManager!!.connectionState + if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) { + bleManager!!.close() + onDeviceDisconnected(bluetoothDevice!!) + return + } + bleManager!!.disconnect().enqueue() + } + + /** + * Sets whether the bound activity if changing configuration or not. + * If `false`, we will turn off battery level notifications in onUnbind(..) method below. + * + * @param changing true if the bound activity is finishing + */ + fun setActivityIsChangingConfiguration(changing: Boolean) { + activityIsChangingConfiguration = changing + } + + /** + * Returns the device address + * + * @return device address + */ + val deviceAddress: String + get() = bluetoothDevice!!.address + + /** + * Returns the device name + * + * @return the device name + */ + fun getDeviceName(): String? { + return deviceName + } + + /** + * Returns the Bluetooth device + * + * @return the Bluetooth device + */ + fun getBluetoothDevice(): BluetoothDevice? { + return bluetoothDevice + } + + /** + * Returns `true` if the device is connected to the sensor. + * + * @return `true` if device is connected to the sensor, `false` otherwise + */ + val isConnected: Boolean + get() = bleManager!!.isConnected + + /** + * Returns the connection state of given device. + * + * @return the connection state, as in [BleManager.getConnectionState]. + */ + val connectionState: Int + get() = bleManager!!.connectionState + + /** + * Returns the log session that can be used to append log entries. + * The log session is created when the service is being created. + * The method returns `null` if the nRF Logger app was not installed. + * + * @return the log session + */ + fun getLogSession(): ILogSession? { + return logSession + } + + override fun log(level: Int, message: String) { + Logger.log(logSession, level, message) + } + + override fun log(level: Int, @StringRes messageRes: Int, vararg params: Any) { + Logger.log(logSession, level, messageRes, *params) + } + }// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation + + /** + * Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity. + * + * @return the service binder + */ + protected val binder: LocalBinder + protected get() =// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation + LocalBinder() + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + bound = true + return binder + } + + override fun onRebind(intent: Intent) { + bound = true + if (!activityIsChangingConfiguration) onRebind() + } + + /** + * Called when the activity has rebound to the service after being recreated. + * This method is not called when the activity was killed to be recreated when the phone orientation changed + * if prior to being killed called [LocalBinder.setActivityIsChangingConfiguration] with parameter true. + */ + protected open fun onRebind() { + // empty default implementation + } + + override fun onUnbind(intent: Intent): Boolean { + bound = false + if (!activityIsChangingConfiguration) onUnbind() + + // We want the onRebind method be called if anything else binds to it again + return true + } + + /** + * Called when the activity has unbound from the service before being finished. + * This method is not called when the activity is killed to be recreated when the phone orientation changed. + */ + protected open fun onUnbind() { + // empty default implementation + } + + override fun onCreate() { + super.onCreate() + handler = Handler() + + // Initialize the manager + bleManager = initializeManager() + + // Register broadcast receivers + registerReceiver( + bluetoothStateBroadcastReceiver, + IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + ) + + // Service has now been created + onServiceCreated() + + // Call onBluetoothEnabled if Bluetooth enabled + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter.isEnabled) { + onBluetoothEnabled() + } + } + + /** + * Called when the service has been created, before the [.onBluetoothEnabled] is called. + */ + protected fun onServiceCreated() { + // empty default implementation + } + + /** + * Initializes the Ble Manager responsible for connecting to a single device. + * + * @return a new BleManager object + */ + protected abstract fun initializeManager(): LoggableBleManager + + /** + * This method returns whether autoConnect option should be used. + * + * @return true to use autoConnect feature, false (default) otherwise. + */ + protected fun shouldAutoConnect(): Boolean { + return false + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + val logUri = intent?.getParcelableExtra(EXTRA_LOG_URI) + logSession = Logger.openSession(applicationContext, logUri) + deviceName = intent?.getStringExtra(EXTRA_DEVICE_NAME) + Logger.i(logSession, "Service started") + val adapter = BluetoothAdapter.getDefaultAdapter() + bleManager!!.setLogger(logSession) + onServiceStarted() + bleManager!!.connect(bluetoothDevice) + .useAutoConnect(shouldAutoConnect()) + .retry(3, 100) + .enqueue() + return START_REDELIVER_INTENT + } + + /** + * Called when the service has been started. The device name and address are set. + * The BLE Manager will try to connect to the device after this method finishes. + */ + protected fun onServiceStarted() { + // empty default implementation + } + + override fun onTaskRemoved(rootIntent: Intent) { + super.onTaskRemoved(rootIntent) + // This method is called when user removed the app from Recents. + // By default, the service will be killed and recreated immediately after that. + // However, all managed devices will be lost and devices will be disconnected. + stopSelf() + } + + override fun onDestroy() { + super.onDestroy() + // Unregister broadcast receivers + unregisterReceiver(bluetoothStateBroadcastReceiver) + + // shutdown the manager + bleManager!!.close() + Logger.i(logSession, "Service destroyed") + bleManager = null + bluetoothDeviceHolder.forgetDevice() + deviceName = null + logSession = null + handler = null + } + + /** + * Method called when Bluetooth Adapter has been disabled. + */ + protected fun onBluetoothDisabled() { + // empty default implementation + } + + /** + * This method is called when Bluetooth Adapter has been enabled and + * after the service was created if Bluetooth Adapter was enabled at that moment. + * This method could initialize all Bluetooth related features, for example open the GATT server. + */ + protected fun onBluetoothEnabled() { + // empty default implementation + } + + override fun onDeviceConnecting(device: BluetoothDevice) { + val broadcast = Intent(BROADCAST_CONNECTION_STATE) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onDeviceConnected(device: BluetoothDevice) { + val broadcast = Intent(BROADCAST_CONNECTION_STATE) + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_DEVICE_NAME, deviceName) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onDeviceDisconnecting(device: BluetoothDevice) { + // Notify user about changing the state to DISCONNECTING + val broadcast = Intent(BROADCAST_CONNECTION_STATE) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + /** + * This method should return false if the service needs to do some asynchronous work after if has disconnected from the device. + * In that case the [.stopService] method must be called when done. + * + * @return true (default) to automatically stop the service when device is disconnected. False otherwise. + */ + protected fun stopWhenDisconnected(): Boolean { + return true + } + + override fun onDeviceDisconnected(device: BluetoothDevice) { + // Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above + + // Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be + // invoked ONLY when user requested disconnection (using Disconnect button). If the device + // disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead. + val broadcast = Intent(BROADCAST_CONNECTION_STATE) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + if (stopWhenDisconnected()) stopService() + } + + protected fun stopService() { + // user requested disconnection. We must stop the service + Logger.v(logSession, "Stopping service...") + stopSelf() + } + + override fun onLinkLossOccurred(device: BluetoothDevice) { + val broadcast = Intent(BROADCAST_CONNECTION_STATE) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onServicesDiscovered(device: BluetoothDevice, optionalServicesFound: Boolean) { + val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true) + broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onDeviceReady(device: BluetoothDevice) { + val broadcast = Intent(BROADCAST_DEVICE_READY) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onDeviceNotSupported(device: BluetoothDevice) { + val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false) + broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + + // no need for disconnecting, it will be disconnected by the manager automatically + } + + override fun onBatteryValueReceived(device: BluetoothDevice, value: Int) { + val broadcast = Intent(BROADCAST_BATTERY_LEVEL) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_BATTERY_LEVEL, value) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onBondingRequired(device: BluetoothDevice) { + showToast(R.string.csc_bonding) + val broadcast = Intent(BROADCAST_BOND_STATE) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onBonded(device: BluetoothDevice) { + showToast(R.string.csc_bonded) + val broadcast = Intent(BROADCAST_BOND_STATE) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onBondingFailed(device: BluetoothDevice) { + showToast(R.string.csc_bonding_failed) + val broadcast = Intent(BROADCAST_BOND_STATE) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + override fun onError(device: BluetoothDevice, message: String, errorCode: Int) { + val broadcast = Intent(BROADCAST_ERROR) + broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) + broadcast.putExtra(EXTRA_ERROR_MESSAGE, message) + broadcast.putExtra(EXTRA_ERROR_CODE, errorCode) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + /** + * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread + * + * @param messageResId an resource id of the message to be shown + */ + protected fun showToast(messageResId: Int) { + handler!!.post { + Toast.makeText(this@BleProfileService, messageResId, Toast.LENGTH_SHORT).show() + } + } + + /** + * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread + * + * @param message a message to be shown + */ + protected fun showToast(message: String?) { + handler!!.post { + Toast.makeText(this@BleProfileService, message, Toast.LENGTH_SHORT).show() + } + } + + /** + * Returns the device address + * + * @return device address + */ + protected val deviceAddress: String + protected get() = bluetoothDevice!!.address + + /** + * Returns `true` if the device is connected to the sensor. + * + * @return `true` if device is connected to the sensor, `false` otherwise + */ + protected val isConnected: Boolean + protected get() = bleManager != null && bleManager!!.isConnected + + companion object { + private const val TAG = "BleProfileService" + const val BROADCAST_CONNECTION_STATE = + "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE" + const val BROADCAST_SERVICES_DISCOVERED = + "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED" + const val BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY" + const val BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE" + + @Deprecated("") + val BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL" + const val BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR" + + /** + * The key for the device name that is returned in [.BROADCAST_CONNECTION_STATE] with state [.STATE_CONNECTED]. + */ + const val EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME" + const val EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE" + const val EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI" + const val EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE" + const val EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE" + const val EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY" + const val EXTRA_SERVICE_SECONDARY = + "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY" + + @Deprecated("") + val EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL" + const val EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE" + const val EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE" + const val STATE_LINK_LOSS = -1 + const val STATE_DISCONNECTED = 0 + const val STATE_CONNECTED = 1 + const val STATE_CONNECTING = 2 + const val STATE_DISCONNECTING = 3 + } +} \ No newline at end of file diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BluetoothDataReadBroadcast.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BluetoothDataReadBroadcast.kt new file mode 100644 index 00000000..3ea5baef --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BluetoothDataReadBroadcast.kt @@ -0,0 +1,19 @@ +package no.nordicsemi.android.service + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow + +abstract class BluetoothDataReadBroadcast { + + private val _event = MutableSharedFlow( + replay = 1, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val events: SharedFlow = _event + + fun offer(newEvent: T) { + _event.tryEmit(newEvent) + } +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt new file mode 100644 index 00000000..9e25f020 --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.service + +import android.app.Notification +import android.app.NotificationManager +import android.os.Build + +abstract class ForegroundBleService> : BleProfileService() { + + protected abstract val manager: T + + override fun onDestroy() { + // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService + cancelNotification() + super.onDestroy() + } + + override fun onRebind() { + stopForegroundService() + if (isConnected) { + // This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property). + // If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications. + manager.readBatteryLevelCharacteristic() + } + } + + override fun onUnbind() { + // When we are connected, but the application is not open, we are not really interested in battery level notifications. + // But we will still be receiving other values, if enabled. + if (isConnected) manager.disableBatteryLevelCharacteristicNotifications() + startForegroundService() + } + + /** + * Sets the service as a foreground service + */ + private fun startForegroundService() { + // when the activity closes we need to show the notification that user is connected to the peripheral sensor + // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services + val notification = createNotification(R.string.csc_notification_connected_message, 0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification) + } else { + val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + nm.notify(NOTIFICATION_ID, notification) + } + } + + /** + * Stops the service as a foreground service + */ + private fun stopForegroundService() { + // when the activity rebinds to the service, remove the notification and stop the foreground service + // on devices running Android 8.0 (Oreo) or above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stopForeground(true) + } else { + cancelNotification() + } + } + + /** + * Creates the notification + * + * @param messageResId the message resource id. The message must have one String parameter,

+ * f.e. `%s is connected` + * @param defaults + */ + private fun createNotification(messageResId: Int, defaults: Int): Notification { + TODO() +// final Intent parentIntent = new Intent(this, FeaturesActivity.class); +// parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +// final Intent targetIntent = new Intent(this, CSCActivity.class); +// +// final Intent disconnect = new Intent(ACTION_DISCONNECT); +// final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); +// +// // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed +// final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); +// final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); +// builder.setContentIntent(pendingIntent); +// builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); +// builder.setSmallIcon(R.drawable.ic_stat_notify_csc); +// builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); +// builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction)); +// +// return builder.build(); + } + + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private fun cancelNotification() { + val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + nm.cancel(NOTIFICATION_ID) + } + + companion object { + private const val NOTIFICATION_ID = 200 + } +} diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/batery/LoggableBleManager.kt b/lib_service/src/main/java/no/nordicsemi/android/service/LoggableBleManager.kt similarity index 73% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/batery/LoggableBleManager.kt rename to lib_service/src/main/java/no/nordicsemi/android/service/LoggableBleManager.kt index 0797216f..37cd2758 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/batery/LoggableBleManager.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/LoggableBleManager.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.csc.batery +package no.nordicsemi.android.service import android.content.Context import android.util.Log @@ -13,17 +13,7 @@ import no.nordicsemi.android.log.Logger * * @param the callbacks class. */ -abstract class LoggableBleManager -/** - * The manager constructor. - * - * - * After constructing the manager, the callbacks object must be set with - * [.setGattCallbacks]. - * - * @param context the context. - */ - (context: Context) : LegacyBleManager(context) { +abstract class LoggableBleManager(context: Context) : LegacyBleManager(context) { private var logSession: ILogSession? = null /** diff --git a/lib_service/src/main/res/values/strings.xml b/lib_service/src/main/res/values/strings.xml new file mode 100644 index 00000000..0d4a7929 --- /dev/null +++ b/lib_service/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Bonding failed. + Bonding with the device… + The device is now bonded. + %s is connected. + diff --git a/lib_broadcast/src/test/java/no/nordicsemi/android/broadcast/ExampleUnitTest.kt b/lib_service/src/test/java/no/nordicsemi/android/service/ExampleUnitTest.kt similarity index 88% rename from lib_broadcast/src/test/java/no/nordicsemi/android/broadcast/ExampleUnitTest.kt rename to lib_service/src/test/java/no/nordicsemi/android/service/ExampleUnitTest.kt index e44cf044..dfd5daa2 100644 --- a/lib_broadcast/src/test/java/no/nordicsemi/android/broadcast/ExampleUnitTest.kt +++ b/lib_service/src/test/java/no/nordicsemi/android/service/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.broadcast +package no.nordicsemi.android.service import org.junit.Test diff --git a/lib_utils/build.gradle b/lib_utils/build.gradle new file mode 100644 index 00000000..dd4dbb45 --- /dev/null +++ b/lib_utils/build.gradle @@ -0,0 +1,2 @@ +apply from: rootProject.file("library.gradle") +apply plugin: 'kotlin-parcelize' diff --git a/lib_events/src/androidTest/java/no/nordicsemi/android/events/ExampleInstrumentedTest.kt b/lib_utils/src/androidTest/java/no/nordicsemi/android/utils/ExampleInstrumentedTest.kt similarity index 82% rename from lib_events/src/androidTest/java/no/nordicsemi/android/events/ExampleInstrumentedTest.kt rename to lib_utils/src/androidTest/java/no/nordicsemi/android/utils/ExampleInstrumentedTest.kt index 226ebf4e..53bad000 100644 --- a/lib_events/src/androidTest/java/no/nordicsemi/android/events/ExampleInstrumentedTest.kt +++ b/lib_utils/src/androidTest/java/no/nordicsemi/android/utils/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.events +package no.nordicsemi.android.utils import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("no.nordicsemi.android.events.test", appContext.packageName) + assertEquals("no.nordicsemi.android.utils.test", appContext.packageName) } } \ No newline at end of file diff --git a/lib_events/src/main/AndroidManifest.xml b/lib_utils/src/main/AndroidManifest.xml similarity index 73% rename from lib_events/src/main/AndroidManifest.xml rename to lib_utils/src/main/AndroidManifest.xml index 8cb5589e..82b96384 100644 --- a/lib_events/src/main/AndroidManifest.xml +++ b/lib_utils/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="no.nordicsemi.android.utils"> \ No newline at end of file diff --git a/lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt b/lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt new file mode 100644 index 00000000..e8dc7b8f --- /dev/null +++ b/lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt @@ -0,0 +1,16 @@ +package no.nordicsemi.android.utils + +import android.app.ActivityManager +import android.content.Context + +val T.exhaustive + get() = this + +val String.Companion.EMPTY + get() = "" + +fun Context.isServiceRunning(serviceClassName: String): Boolean { + val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val services = activityManager.getRunningServices(Integer.MAX_VALUE) + return services.find { it.service.className == serviceClassName } != null +} diff --git a/lib_events/src/test/java/no/nordicsemi/android/events/ExampleUnitTest.kt b/lib_utils/src/test/java/no/nordicsemi/android/utils/ExampleUnitTest.kt similarity index 89% rename from lib_events/src/test/java/no/nordicsemi/android/events/ExampleUnitTest.kt rename to lib_utils/src/test/java/no/nordicsemi/android/utils/ExampleUnitTest.kt index 8721ee3c..b4c1d93e 100644 --- a/lib_events/src/test/java/no/nordicsemi/android/events/ExampleUnitTest.kt +++ b/lib_utils/src/test/java/no/nordicsemi/android/utils/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.events +package no.nordicsemi.android.utils import org.junit.Test diff --git a/settings.gradle b/settings.gradle index 4b73fdde..5ca27f41 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,7 @@ dependencyResolutionManagement { alias('nordic-log').to('no.nordicsemi.android:log:2.3.0') alias('nordic-scanner').to('no.nordicsemi.android.support.v18:scanner:1.5.0') + alias('localbroadcastmanager').to('androidx.localbroadcastmanager:localbroadcastmanager:1.0.0') alias('material').to('com.google.android.material:material:1.4.0') version('lifecycle', '2.3.1') @@ -57,12 +58,14 @@ dependencyResolutionManagement { rootProject.name = "Android-nRF-Toolbox" include ':app' + include ':feature_csc' -include ':lib_broadcast' -include ':lib_events' +include ':feature_scanner' + +include ':lib_service' +include ':lib_theme' +include ':lib_utils' if (file('../Android-BLE-Library').exists()) { includeBuild('../Android-BLE-Library') } -include ':lib_theme' -include ':lib_scanner'