From e36b7a1e0c43fed9d25e6dd065432d1f259c484b Mon Sep 17 00:00:00 2001 From: "Rajaratnam, Roshan" Date: Tue, 6 Aug 2019 15:15:36 +0200 Subject: [PATCH] start foreground service --- app/src/main/AndroidManifest.xml | 1 + .../android/nrftoolbox/cgms/CGMService.java | 487 +++++++++-------- .../android/nrftoolbox/csc/CSCService.java | 306 ++++++----- .../android/nrftoolbox/hts/HTSService.java | 46 +- .../android/nrftoolbox/rsc/RSCService.java | 377 +++++++------ .../nrftoolbox/template/TemplateService.java | 257 +++++---- .../android/nrftoolbox/uart/UARTService.java | 500 ++++++++++-------- 7 files changed, 1070 insertions(+), 904 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 795435df..aef5c37b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,7 @@ + getRecords() { - return mManager.getRecords(); - } + public class CGMSBinder extends LocalBinder { + /** + * Returns all records as a sparse array where sequence number is the key. + * + * @return the records list + */ + public SparseArray getRecords() { + return mManager.getRecords(); + } - /** - * Clears the records list locally - */ - public void clear() { - if (mManager != null) - mManager.clear(); - } + /** + * Clears the records list locally + */ + public void clear() { + if (mManager != null) + mManager.clear(); + } - /** - * Sends the request to obtain the first (oldest) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control - * Point indication with status code ({@link CGMSManager# RESPONSE_SUCCESS} or other in case of error. - */ - public void getFirstRecord() { - if (mManager != null) - mManager.getFirstRecord(); - } + /** + * Sends the request to obtain the first (oldest) record from glucose device. + * The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control + * Point indication with status code ({@link CGMSManager# RESPONSE_SUCCESS} or other in case of error. + */ + public void getFirstRecord() { + if (mManager != null) + mManager.getFirstRecord(); + } - /** - * Sends the request to obtain the last (most recent) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access - * Control Point indication with status code Success or other in case of error. - */ - public void getLastRecord() { - if (mManager != null) - mManager.getLastRecord(); - } + /** + * Sends the request to obtain the last (most recent) record from glucose device. + * The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access + * Control Point indication with status code Success or other in case of error. + */ + public void getLastRecord() { + if (mManager != null) + mManager.getLastRecord(); + } - /** - * Sends the request to obtain all records from glucose device. - * Initially we want to notify user about the number of the records so the Report Number of Stored Records is send. - * The data will be returned to Glucose Measurement characteristic as a series of notifications followed - * by Record Access Control Point indication with status code Success or other in case of error. - */ - public void getAllRecords() { - if (mManager != null) - mManager.getAllRecords(); - } + /** + * Sends the request to obtain all records from glucose device. + * Initially we want to notify user about the number of the records so the Report Number of Stored Records is send. + * The data will be returned to Glucose Measurement characteristic as a series of notifications followed + * by Record Access Control Point indication with status code Success or other in case of error. + */ + public void getAllRecords() { + if (mManager != null) + mManager.getAllRecords(); + } - /** - * Sends the request to obtain all records from glucose device with sequence number greater - * than the last one already obtained. The data will be returned to Glucose Measurement - * characteristic as a series of notifications followed by Record Access Control Point - * indication with status code Success or other in case of error. - */ - public void refreshRecords() { - if (mManager != null) - mManager.refreshRecords(); - } + /** + * Sends the request to obtain all records from glucose device with sequence number greater + * than the last one already obtained. The data will be returned to Glucose Measurement + * characteristic as a series of notifications followed by Record Access Control Point + * indication with status code Success or other in case of error. + */ + public void refreshRecords() { + if (mManager != null) + mManager.refreshRecords(); + } - /** - * Sends abort operation signal to the device - */ - public void abort() { - if (mManager != null) - mManager.abort(); - } + /** + * Sends abort operation signal to the device + */ + public void abort() { + if (mManager != null) + mManager.abort(); + } - /** - * Sends Delete op code with All stored records parameter. This method may not be supported by the SDK sample. - */ - public void deleteAllRecords() { - if (mManager != null) - mManager.deleteAllRecords(); - } - } + /** + * Sends Delete op code with All stored records parameter. This method may not be supported by the SDK sample. + */ + public void deleteAllRecords() { + if (mManager != null) + mManager.deleteAllRecords(); + } + } - @Override - protected LocalBinder getBinder() { - return mBinder; - } + @Override + protected LocalBinder getBinder() { + return mBinder; + } - @Override - protected LoggableBleManager initializeManager() { - return mManager = new CGMSManager(this); - } + @Override + protected LoggableBleManager initializeManager() { + return mManager = new CGMSManager(this); + } - @Override - public void onCreate() { - super.onCreate(); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(mDisconnectActionBroadcastReceiver, filter); - } + @Override + public void onCreate() { + super.onCreate(); + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DISCONNECT); + registerReceiver(mDisconnectActionBroadcastReceiver, filter); + } - @Override - public void 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(mDisconnectActionBroadcastReceiver); + @Override + public void onDestroy() { + // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService + stopForegroundService(); + unregisterReceiver(mDisconnectActionBroadcastReceiver); - super.onDestroy(); - } + super.onDestroy(); + } - @Override - protected void onRebind() { - // when the activity rebinds to the service, remove the notification - cancelNotification(); - } + @Override + protected void onRebind() { + startForegroundService(); + } - @Override - protected void onUnbind() { - // when the activity closes we need to show the notification that user is connected to the sensor - createNotification(R.string.csc_notification_connected_message, 0); - } + @Override + protected void onUnbind() { + startForegroundService(); + } - /** - * Creates the notification - * - * @param messageResId the message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults signals that will be used to notify the user - */ - private void createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, CGMSActivity.class); + /** + * Sets the service as a foreground service + */ + private void 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 + final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification); + } else { + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + } - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + /** + * Stops the service as a foreground service + */ + private void 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(); + } + } - // 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_cgms); - 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)); + /** + * Creates the notification + * + * @param messageResId the message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults signals that will be used to notify the user + */ + private Notification createNotification(final int messageResId, final int defaults) { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, CGMSActivity.class); - final Notification notification = builder.build(); - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } + final Intent disconnect = new Intent(ACTION_DISCONNECT); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } + // 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_cgms); + 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)); - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; + return builder.build(); + } - @Override - public void onCGMValueReceived(@NonNull final BluetoothDevice device, final CGMSRecord record) { - final Intent broadcast = new Intent(BROADCAST_NEW_CGMS_VALUE); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_CGMS_RECORD, record); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotification() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + } - @Override - public void onOperationStarted(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_STARTED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + /** + * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. + */ + private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); + if (isConnected()) + getBinder().disconnect(); + else + stopSelf(); + } + }; - @Override - public void onOperationCompleted(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_COMPLETED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onCGMValueReceived(@NonNull final BluetoothDevice device, final CGMSRecord record) { + final Intent broadcast = new Intent(BROADCAST_NEW_CGMS_VALUE); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_CGMS_RECORD, record); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onOperationFailed(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_FAILED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onOperationStarted(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(OPERATION_STARTED); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onOperationAborted(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_ABORTED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onOperationCompleted(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(OPERATION_COMPLETED); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onOperationNotSupported(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_NOT_SUPPORTED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, false); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onOperationFailed(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(OPERATION_FAILED); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onDatasetCleared(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_DATA_SET_CLEAR); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onOperationAborted(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(OPERATION_ABORTED); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onNumberOfRecordsRequested(@NonNull final BluetoothDevice device, final int value) { - if (value == 0) - showToast(R.string.gls_progress_zero); - else - showToast(getResources().getQuantityString(R.plurals.gls_progress, value, value)); + @Override + public void onOperationNotSupported(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(OPERATION_NOT_SUPPORTED); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, false); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - } + @Override + public void onDatasetCleared(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_DATA_SET_CLEAR); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onNumberOfRecordsRequested(@NonNull final BluetoothDevice device, final int value) { + if (value == 0) + showToast(R.string.gls_progress_zero); + else + showToast(getResources().getQuantityString(R.plurals.gls_progress, value, value)); + + } + + @Override + public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { + final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java index 2a99f0c3..e29bc71d 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java @@ -30,10 +30,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; + import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; - import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; import no.nordicsemi.android.nrftoolbox.R; @@ -42,168 +43,195 @@ import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; public class CSCService extends BleProfileService implements CSCManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "CSCService"; + @SuppressWarnings("unused") + private static final String TAG = "CSCService"; - public static final String BROADCAST_WHEEL_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_WHEEL_DATA"; - /** Speed in meters per second. */ - public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_SPEED"; - /** Distance in meters. */ - public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_DISTANCE"; - /** Total distance in meters. */ - public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_TOTAL_DISTANCE"; + public static final String BROADCAST_WHEEL_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_WHEEL_DATA"; + /** + * Speed in meters per second. + */ + public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_SPEED"; + /** + * Distance in meters. + */ + public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_DISTANCE"; + /** + * Total distance in meters. + */ + public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_TOTAL_DISTANCE"; - public static final String BROADCAST_CRANK_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_CRANK_DATA"; - public static final String EXTRA_GEAR_RATIO = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_GEAR_RATIO"; - public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_CADENCE"; + public static final String BROADCAST_CRANK_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_CRANK_DATA"; + public static final String EXTRA_GEAR_RATIO = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_GEAR_RATIO"; + public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_CADENCE"; - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; + public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; + public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - private static final String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.csc.ACTION_DISCONNECT"; + private static final String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.csc.ACTION_DISCONNECT"; - private final static int NOTIFICATION_ID = 200; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; + private final static int NOTIFICATION_ID = 200; + private final static int OPEN_ACTIVITY_REQ = 0; + private final static int DISCONNECT_REQ = 1; - private final LocalBinder mBinder = new CSCBinder(); - private CSCManager mManager; + private final LocalBinder mBinder = new CSCBinder(); + private CSCManager mManager; - /** - * This local binder is an interface for the bonded activity to operate with the RSC sensor - */ - class CSCBinder extends LocalBinder { - // empty - } + /** + * This local binder is an interface for the bonded activity to operate with the RSC sensor + */ + class CSCBinder extends LocalBinder { + // empty + } - @Override - protected LocalBinder getBinder() { - return mBinder; - } + @Override + protected LocalBinder getBinder() { + return mBinder; + } - @Override - protected LoggableBleManager initializeManager() { - return mManager = new CSCManager(this); - } + @Override + protected LoggableBleManager initializeManager() { + return mManager = new CSCManager(this); + } - @Override - public void onCreate() { - super.onCreate(); + @Override + public void onCreate() { + super.onCreate(); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(mDisconnectActionBroadcastReceiver, filter); - } + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DISCONNECT); + registerReceiver(mDisconnectActionBroadcastReceiver, filter); + } - @Override - public void 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(mDisconnectActionBroadcastReceiver); + @Override + public void 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(mDisconnectActionBroadcastReceiver); - super.onDestroy(); - } + super.onDestroy(); + } - @Override - protected void onRebind() { - // when the activity rebinds to the service, remove the notification - cancelNotification(); + @Override + protected void 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. - mManager.readBatteryLevelCharacteristic(); - } - } + 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. + mManager.readBatteryLevelCharacteristic(); + } + } - @Override - protected void 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()) - mManager.disableBatteryLevelCharacteristicNotifications(); + @Override + protected void 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()) + mManager.disableBatteryLevelCharacteristicNotifications(); + startForegroundService(); + } - // when the activity closes we need to show the notification that user is connected to the sensor - createNotification(R.string.csc_notification_connected_message, 0); - } + @Override + public void onDistanceChanged(@NonNull final BluetoothDevice device, final float totalDistance, final float distance, final float speed) { + final Intent broadcast = new Intent(BROADCAST_WHEEL_DATA); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_SPEED, speed); + broadcast.putExtra(EXTRA_DISTANCE, distance); + broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onDistanceChanged(@NonNull final BluetoothDevice device, final float totalDistance, final float distance, final float speed) { - final Intent broadcast = new Intent(BROADCAST_WHEEL_DATA); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_SPEED, speed); - broadcast.putExtra(EXTRA_DISTANCE, distance); - broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onCrankDataChanged(@NonNull final BluetoothDevice device, final float crankCadence, final float gearRatio) { + final Intent broadcast = new Intent(BROADCAST_CRANK_DATA); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_GEAR_RATIO, gearRatio); + broadcast.putExtra(EXTRA_CADENCE, (int) crankCadence); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onCrankDataChanged(@NonNull final BluetoothDevice device, final float crankCadence, final float gearRatio) { - final Intent broadcast = new Intent(BROADCAST_CRANK_DATA); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_GEAR_RATIO, gearRatio); - broadcast.putExtra(EXTRA_CADENCE, (int) crankCadence); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { + final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + /** + * Sets the service as a foreground service + */ + private void 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 + final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification); + } else { + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + } - /** - * Creates the notification - * - * @param messageResId - * the message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - * signals that will be used to notify the user - */ - private void createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, CSCActivity.class); + /** + * Stops the service as a foreground service + */ + private void 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(); + } + } - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + /** + * Creates the notification + * + * @param messageResId the message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults + */ + private Notification createNotification(final int messageResId, final int defaults) { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, CSCActivity.class); - // 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)); + final Intent disconnect = new Intent(ACTION_DISCONNECT); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - final Notification notification = builder.build(); - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } + // 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)); - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } + return builder.build(); + } - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotification() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + } + + /** + * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. + */ + private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); + if (isConnected()) + getBinder().disconnect(); + else + stopSelf(); + } + }; } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hts/HTSService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/hts/HTSService.java index 811740ea..2f9ed139 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hts/HTSService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/hts/HTSService.java @@ -30,6 +30,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -110,14 +112,12 @@ public class HTSService extends BleProfileService implements HTSManagerCallbacks @Override protected void onRebind() { - // when the activity rebinds to the service, remove the notification - cancelNotification(); + stopForegroundService(); } @Override protected void onUnbind() { - // when the activity closes we need to show the notification that user is connected to the sensor - createNotification(R.string.hts_notification_connected_message, 0); + startForegroundService(); } @Override @@ -153,16 +153,42 @@ public class HTSService extends BleProfileService implements HTSManagerCallbacks LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } + /** + * Sets the service as a foreground service + */ + private void 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 + final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification); + } else { + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + } + + /** + * Stops the service as a foreground service + */ + private void 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 + * @param messageResId * message resource id. The message must have one String parameter,
* f.e. <string name="name">%s is connected</string> * @param defaults - * signals that will be used to notify the user */ - private void createNotification(final int messageResId, final int defaults) { + private Notification createNotification(final int messageResId, final int defaults) { final Intent parentIntent = new Intent(this, FeaturesActivity.class); parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); final Intent targetIntent = new Intent(this, HTSActivity.class); @@ -179,9 +205,7 @@ public class HTSService extends BleProfileService implements HTSManagerCallbacks builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.hts_notification_action_disconnect), disconnectAction)); - final Notification notification = builder.build(); - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); + return builder.build(); } /** diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java index 04480330..a7b6f369 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java @@ -30,12 +30,13 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; import android.os.Handler; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; - import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; import no.nordicsemi.android.nrftoolbox.R; @@ -44,209 +45,239 @@ import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; public class RSCService extends BleProfileService implements RSCManagerCallbacks { - private static final String TAG = "RSCService"; + private static final String TAG = "RSCService"; - public static final String BROADCAST_RSC_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_RSC_MEASUREMENT"; - public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_SPEED"; - public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_CADENCE"; - public static final String EXTRA_STRIDE_LENGTH = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDE_LENGTH"; - public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_TOTAL_DISTANCE"; - public static final String EXTRA_ACTIVITY = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_ACTIVITY"; + public static final String BROADCAST_RSC_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_RSC_MEASUREMENT"; + public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_SPEED"; + public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_CADENCE"; + public static final String EXTRA_STRIDE_LENGTH = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDE_LENGTH"; + public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_TOTAL_DISTANCE"; + public static final String EXTRA_ACTIVITY = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_ACTIVITY"; - public static final String BROADCAST_STRIDES_UPDATE = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_STRIDES_UPDATE"; - public static final String EXTRA_STRIDES = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDES"; - public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_DISTANCE"; + public static final String BROADCAST_STRIDES_UPDATE = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_STRIDES_UPDATE"; + public static final String EXTRA_STRIDES = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDES"; + public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_DISTANCE"; - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; + public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; + public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.rsc.ACTION_DISCONNECT"; + private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.rsc.ACTION_DISCONNECT"; - private RSCManager mManager; + private RSCManager mManager; - /** The last value of a cadence */ - private float mCadence; - /** Trip distance in cm */ - private long mDistance; - /** Stride length in cm */ - private Integer mStrideLength; - /** Number of steps in the trip */ - private int mStepsNumber; - private boolean mTaskInProgress; - private final Handler mHandler = new Handler(); + /** + * The last value of a cadence + */ + private float mCadence; + /** + * Trip distance in cm + */ + private long mDistance; + /** + * Stride length in cm + */ + private Integer mStrideLength; + /** + * Number of steps in the trip + */ + private int mStepsNumber; + private boolean mTaskInProgress; + private final Handler mHandler = new Handler(); - private final static int NOTIFICATION_ID = 200; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; + private final static int NOTIFICATION_ID = 200; + private final static int OPEN_ACTIVITY_REQ = 0; + private final static int DISCONNECT_REQ = 1; - private final LocalBinder mBinder = new RSCBinder(); + private final LocalBinder mBinder = new RSCBinder(); - /** - * This local binder is an interface for the bound activity to operate with the RSC sensor. - */ - class RSCBinder extends LocalBinder { - // empty - } + /** + * This local binder is an interface for the bound activity to operate with the RSC sensor. + */ + class RSCBinder extends LocalBinder { + // empty + } - @Override - protected LocalBinder getBinder() { - return mBinder; - } + @Override + protected LocalBinder getBinder() { + return mBinder; + } - @Override - protected LoggableBleManager initializeManager() { - return mManager = new RSCManager(this); - } + @Override + protected LoggableBleManager initializeManager() { + return mManager = new RSCManager(this); + } - @Override - public void onCreate() { - super.onCreate(); + @Override + public void onCreate() { + super.onCreate(); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(mDisconnectActionBroadcastReceiver, filter); - } + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DISCONNECT); + registerReceiver(mDisconnectActionBroadcastReceiver, filter); + } - @Override - public void 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(mDisconnectActionBroadcastReceiver); + @Override + public void onDestroy() { + // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService + stopForegroundService(); + unregisterReceiver(mDisconnectActionBroadcastReceiver); - super.onDestroy(); - } + super.onDestroy(); + } - @Override - protected void onRebind() { - // when the activity rebinds to the service, remove the notification - cancelNotification(); + @Override + protected void 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. - mManager.readBatteryLevelCharacteristic(); - } - } + 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. + mManager.readBatteryLevelCharacteristic(); + } + } - @Override - protected void 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()) - mManager.disableBatteryLevelCharacteristicNotifications(); + @Override + protected void 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()) + mManager.disableBatteryLevelCharacteristicNotifications(); - // when the activity closes we need to show the notification that user is connected to the sensor - createNotification(R.string.rsc_notification_connected_message, 0); - } + startForegroundService(); + } - private final Runnable mUpdateStridesTask = new Runnable() { - @Override - public void run() { - if (!isConnected()) - return; + private final Runnable mUpdateStridesTask = new Runnable() { + @Override + public void run() { + if (!isConnected()) + return; - mStepsNumber++; - mDistance += mStrideLength; // [cm] - final Intent broadcast = new Intent(BROADCAST_STRIDES_UPDATE); - broadcast.putExtra(EXTRA_STRIDES, mStepsNumber); - broadcast.putExtra(EXTRA_DISTANCE, mDistance); - LocalBroadcastManager.getInstance(RSCService.this).sendBroadcast(broadcast); + mStepsNumber++; + mDistance += mStrideLength; // [cm] + final Intent broadcast = new Intent(BROADCAST_STRIDES_UPDATE); + broadcast.putExtra(EXTRA_STRIDES, mStepsNumber); + broadcast.putExtra(EXTRA_DISTANCE, mDistance); + LocalBroadcastManager.getInstance(RSCService.this).sendBroadcast(broadcast); - if (mCadence > 0) { - final long interval = (long) (1000.0f * 60.0f / mCadence); - mHandler.postDelayed(mUpdateStridesTask, interval); - } else { - mTaskInProgress = false; - } - } - }; + if (mCadence > 0) { + final long interval = (long) (1000.0f * 60.0f / mCadence); + mHandler.postDelayed(mUpdateStridesTask, interval); + } else { + mTaskInProgress = false; + } + } + }; - @Override - public void onRSCMeasurementReceived(@NonNull final BluetoothDevice device, final boolean running, - final float instantaneousSpeed, final int instantaneousCadence, - @Nullable final Integer strideLength, - @Nullable final Long totalDistance) { - final Intent broadcast = new Intent(BROADCAST_RSC_MEASUREMENT); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_SPEED, instantaneousSpeed); - broadcast.putExtra(EXTRA_CADENCE, instantaneousCadence); - broadcast.putExtra(EXTRA_STRIDE_LENGTH, strideLength); - broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance); - broadcast.putExtra(EXTRA_ACTIVITY, running); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + @Override + public void onRSCMeasurementReceived(@NonNull final BluetoothDevice device, final boolean running, + final float instantaneousSpeed, final int instantaneousCadence, + @Nullable final Integer strideLength, + @Nullable final Long totalDistance) { + final Intent broadcast = new Intent(BROADCAST_RSC_MEASUREMENT); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_SPEED, instantaneousSpeed); + broadcast.putExtra(EXTRA_CADENCE, instantaneousCadence); + broadcast.putExtra(EXTRA_STRIDE_LENGTH, strideLength); + broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance); + broadcast.putExtra(EXTRA_ACTIVITY, running); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - // Start strides counter if not in progress - mCadence = instantaneousCadence; - if (strideLength != null) { - mStrideLength = strideLength; - } - if (!mTaskInProgress && strideLength != null && instantaneousCadence > 0) { - mTaskInProgress = true; + // Start strides counter if not in progress + mCadence = instantaneousCadence; + if (strideLength != null) { + mStrideLength = strideLength; + } + if (!mTaskInProgress && strideLength != null && instantaneousCadence > 0) { + mTaskInProgress = true; - final long interval = (long) (1000.0f * 60.0f / mCadence); - mHandler.postDelayed(mUpdateStridesTask, interval); - } - } + final long interval = (long) (1000.0f * 60.0f / mCadence); + mHandler.postDelayed(mUpdateStridesTask, interval); + } + } - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int value) { + final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - /** - * Creates the notification - * - * @param messageResId - * message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - * signals that will be used to notify the user - */ - private void createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, RSCActivity.class); + /** + * Sets the service as a foreground service + */ + private void 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 + final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification); + } else { + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + } - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + /** + * Stops the service as a foreground service + */ + private void 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(); + } + } - // 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_rsc); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.rsc_notification_action_disconnect), disconnectAction)); + /** + * Creates the notification + * + * @param messageResId message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults + */ + private Notification createNotification(final int messageResId, final int defaults) { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, RSCActivity.class); - final Notification notification = builder.build(); - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } + final Intent disconnect = new Intent(ACTION_DISCONNECT); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } + // 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_rsc); + builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.rsc_notification_action_disconnect), disconnectAction)); - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; + return builder.build(); + } + + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotification() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + } + + /** + * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. + */ + private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); + if (isConnected()) + getBinder().disconnect(); + else + stopSelf(); + } + }; } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java index e6be074e..069d93c9 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java @@ -30,10 +30,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; + import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; - import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; import no.nordicsemi.android.nrftoolbox.R; @@ -42,144 +43,168 @@ import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; public class TemplateService extends BleProfileService implements TemplateManagerCallbacks { - public static final String BROADCAST_TEMPLATE_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.template.BROADCAST_MEASUREMENT"; - public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.template.EXTRA_DATA"; + public static final String BROADCAST_TEMPLATE_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.template.BROADCAST_MEASUREMENT"; + public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.template.EXTRA_DATA"; - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; + public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; + public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.template.ACTION_DISCONNECT"; + private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.template.ACTION_DISCONNECT"; - private final static int NOTIFICATION_ID = 864; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; + private final static int NOTIFICATION_ID = 864; + private final static int OPEN_ACTIVITY_REQ = 0; + private final static int DISCONNECT_REQ = 1; - private TemplateManager mManager; + private TemplateManager mManager; - private final LocalBinder mBinder = new TemplateBinder(); + private final LocalBinder mBinder = new TemplateBinder(); - /** - * This local binder is an interface for the bound activity to operate with the sensor. - */ - class TemplateBinder extends LocalBinder { - // TODO Define service API that may be used by a bound Activity + /** + * This local binder is an interface for the bound activity to operate with the sensor. + */ + class TemplateBinder extends LocalBinder { + // TODO Define service API that may be used by a bound Activity - /** - * Sends some important data to the device. - * - * @param parameter some parameter. - */ - public void performAction(final String parameter) { - mManager.performAction(parameter); - } - } + /** + * Sends some important data to the device. + * + * @param parameter some parameter. + */ + public void performAction(final String parameter) { + mManager.performAction(parameter); + } + } - @Override - protected LocalBinder getBinder() { - return mBinder; - } + @Override + protected LocalBinder getBinder() { + return mBinder; + } - @Override - protected LoggableBleManager initializeManager() { - return mManager = new TemplateManager(this); - } + @Override + protected LoggableBleManager initializeManager() { + return mManager = new TemplateManager(this); + } - @Override - public void onCreate() { - super.onCreate(); + @Override + public void onCreate() { + super.onCreate(); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(mDisconnectActionBroadcastReceiver, filter); - } + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DISCONNECT); + registerReceiver(mDisconnectActionBroadcastReceiver, filter); + } - @Override - public void 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(mDisconnectActionBroadcastReceiver); + @Override + public void onDestroy() { + // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService + stopForegroundService(); + unregisterReceiver(mDisconnectActionBroadcastReceiver); - super.onDestroy(); - } + super.onDestroy(); + } - @Override - protected void onRebind() { - // when the activity rebinds to the service, remove the notification - cancelNotification(); - } + @Override + protected void onRebind() { + stopForegroundService(); + } - @Override - protected void onUnbind() { - // when the activity closes we need to show the notification that user is connected to the sensor - createNotification(R.string.template_notification_connected_message, 0); - } + @Override + protected void onUnbind() { + startForegroundService(); + } - @Override - public void onSampleValueReceived(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_TEMPLATE_MEASUREMENT); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + @Override + public void onSampleValueReceived(@NonNull final BluetoothDevice device, final int value) { + final Intent broadcast = new Intent(BROADCAST_TEMPLATE_MEASUREMENT); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, value); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - if (!mBound) { - // Here we may update the notification to display the current value. - // TODO modify the notification here - } - } + if (!mBound) { + // Here we may update the notification to display the current value. + // TODO modify the notification here + } + } - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { + @Override + public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - } + } - /** - * Creates the notification. - * - * @param messageResId message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults signals that will be used to notify the user - */ - private void createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, TemplateActivity.class); + /** + * Sets the service as a foreground service + */ + private void 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 + final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification); + } else { + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + } - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + /** + * Stops the service as a foreground service + */ + private void 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(); + } + } - // 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_template); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.template_notification_action_disconnect), disconnectAction)); + /** + * Creates the notification. + * + * @param messageResId message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults signals that will be used to notify the user + */ + private Notification createNotification(final int messageResId, final int defaults) { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, TemplateActivity.class); - final Notification notification = builder.build(); - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } + final Intent disconnect = new Intent(ACTION_DISCONNECT); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } + // 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_template); + builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.template_notification_action_disconnect), disconnectAction)); - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; + return builder.build(); + } + + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotification() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + } + + /** + * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. + */ + private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); + if (isConnected()) + getBinder().disconnect(); + else + stopSelf(); + } + }; } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java index 2e885ff7..9e97d53e 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java @@ -30,9 +30,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.os.Build; import android.text.TextUtils; import android.util.Log; @@ -42,6 +40,9 @@ import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.NodeApi; import com.google.android.gms.wearable.Wearable; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; import no.nordicsemi.android.nrftoolbox.R; @@ -51,269 +52,300 @@ import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; import no.nordicsemi.android.nrftoolbox.wearable.common.Constants; public class UARTService extends BleProfileService implements UARTManagerCallbacks { - private static final String TAG = "UARTService"; + private static final String TAG = "UARTService"; - public static final String BROADCAST_UART_TX = "no.nordicsemi.android.nrftoolbox.uart.BROADCAST_UART_TX"; - public static final String BROADCAST_UART_RX = "no.nordicsemi.android.nrftoolbox.uart.BROADCAST_UART_RX"; - public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.uart.EXTRA_DATA"; + public static final String BROADCAST_UART_TX = "no.nordicsemi.android.nrftoolbox.uart.BROADCAST_UART_TX"; + public static final String BROADCAST_UART_RX = "no.nordicsemi.android.nrftoolbox.uart.BROADCAST_UART_RX"; + public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.uart.EXTRA_DATA"; - /** A broadcast message with this action and the message in {@link Intent#EXTRA_TEXT} will be sent t the UART device. */ - public final static String ACTION_SEND = "no.nordicsemi.android.nrftoolbox.uart.ACTION_SEND"; - /** A broadcast message with this action is triggered when a message is received from the UART device. */ - private final static String ACTION_RECEIVE = "no.nordicsemi.android.nrftoolbox.uart.ACTION_RECEIVE"; - /** Action send when user press the DISCONNECT button on the notification. */ - public final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.uart.ACTION_DISCONNECT"; - /** A source of an action. */ - public final static String EXTRA_SOURCE = "no.nordicsemi.android.nrftoolbox.uart.EXTRA_SOURCE"; - public final static int SOURCE_NOTIFICATION = 0; - public final static int SOURCE_WEARABLE = 1; - public final static int SOURCE_3RD_PARTY = 2; + /** + * A broadcast message with this action and the message in {@link Intent#EXTRA_TEXT} will be sent t the UART device. + */ + public final static String ACTION_SEND = "no.nordicsemi.android.nrftoolbox.uart.ACTION_SEND"; + /** + * A broadcast message with this action is triggered when a message is received from the UART device. + */ + private final static String ACTION_RECEIVE = "no.nordicsemi.android.nrftoolbox.uart.ACTION_RECEIVE"; + /** + * Action send when user press the DISCONNECT button on the notification. + */ + public final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.uart.ACTION_DISCONNECT"; + /** + * A source of an action. + */ + public final static String EXTRA_SOURCE = "no.nordicsemi.android.nrftoolbox.uart.EXTRA_SOURCE"; + public final static int SOURCE_NOTIFICATION = 0; + public final static int SOURCE_WEARABLE = 1; + public final static int SOURCE_3RD_PARTY = 2; - private final static int NOTIFICATION_ID = 349; // random - private final static int OPEN_ACTIVITY_REQ = 67; // random - private final static int DISCONNECT_REQ = 97; // random + private final static int NOTIFICATION_ID = 349; // random + private final static int OPEN_ACTIVITY_REQ = 67; // random + private final static int DISCONNECT_REQ = 97; // random - private GoogleApiClient mGoogleApiClient; - private UARTManager mManager; + private GoogleApiClient mGoogleApiClient; + private UARTManager mManager; - private final LocalBinder mBinder = new UARTBinder(); + private final LocalBinder mBinder = new UARTBinder(); - public class UARTBinder extends LocalBinder implements UARTInterface { - @Override - public void send(final String text) { - mManager.send(text); - } - } + public class UARTBinder extends LocalBinder implements UARTInterface { + @Override + public void send(final String text) { + mManager.send(text); + } + } - @Override - protected LocalBinder getBinder() { - return mBinder; - } + @Override + protected LocalBinder getBinder() { + return mBinder; + } - @Override - protected LoggableBleManager initializeManager() { - return mManager = new UARTManager(this); - } + @Override + protected LoggableBleManager initializeManager() { + return mManager = new UARTManager(this); + } - @Override - protected boolean shouldAutoConnect() { - return true; - } + @Override + protected boolean shouldAutoConnect() { + return true; + } - @Override - public void onCreate() { - super.onCreate(); + @Override + public void onCreate() { + super.onCreate(); - registerReceiver(mDisconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT)); - registerReceiver(mIntentBroadcastReceiver, new IntentFilter(ACTION_SEND)); + registerReceiver(mDisconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT)); + registerReceiver(mIntentBroadcastReceiver, new IntentFilter(ACTION_SEND)); - mGoogleApiClient = new GoogleApiClient.Builder(this) - .addApi(Wearable.API) - .build(); - mGoogleApiClient.connect(); - } + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .build(); + mGoogleApiClient.connect(); + } - @Override - public void 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(mDisconnectActionBroadcastReceiver); - unregisterReceiver(mIntentBroadcastReceiver); + @Override + public void onDestroy() { + // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService + stopForegroundService(); + unregisterReceiver(mDisconnectActionBroadcastReceiver); + unregisterReceiver(mIntentBroadcastReceiver); - mGoogleApiClient.disconnect(); + mGoogleApiClient.disconnect(); - super.onDestroy(); - } + super.onDestroy(); + } - @Override - protected void onRebind() { - // when the activity rebinds to the service, remove the notification - cancelNotification(); - } + @Override + protected void onRebind() { + stopForegroundService(); + } - @Override - protected void onUnbind() { - // when the activity closes we need to show the notification that user is connected to the sensor - createNotification(R.string.uart_notification_connected_message, 0); - } + @Override + protected void onUnbind() { + startForegroundService(); + } - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - super.onDeviceConnected(device); - sendMessageToWearables(Constants.UART.DEVICE_CONNECTED, notNull(getDeviceName())); - } + @Override + public void onDeviceConnected(@NonNull final BluetoothDevice device) { + super.onDeviceConnected(device); + sendMessageToWearables(Constants.UART.DEVICE_CONNECTED, notNull(getDeviceName())); + } - @Override - protected boolean stopWhenDisconnected() { - return false; - } + @Override + protected boolean stopWhenDisconnected() { + return false; + } - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - sendMessageToWearables(Constants.UART.DEVICE_DISCONNECTED, notNull(getDeviceName())); - } + @Override + public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { + super.onDeviceDisconnected(device); + sendMessageToWearables(Constants.UART.DEVICE_DISCONNECTED, notNull(getDeviceName())); + } - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - super.onLinkLossOccurred(device); - sendMessageToWearables(Constants.UART.DEVICE_LINKLOSS, notNull(getDeviceName())); - } + @Override + public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { + super.onLinkLossOccurred(device); + sendMessageToWearables(Constants.UART.DEVICE_LINKLOSS, notNull(getDeviceName())); + } - private String notNull(final String name) { - if (!TextUtils.isEmpty(name)) - return name; - return getString(R.string.not_available); - } + private String notNull(final String name) { + if (!TextUtils.isEmpty(name)) + return name; + return getString(R.string.not_available); + } - @Override - public void onDataReceived(final BluetoothDevice device, final String data) { - final Intent broadcast = new Intent(BROADCAST_UART_RX); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, data); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + @Override + public void onDataReceived(final BluetoothDevice device, final String data) { + final Intent broadcast = new Intent(BROADCAST_UART_RX); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, data); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - // send the data received to other apps, e.g. the Tasker - final Intent globalBroadcast = new Intent(ACTION_RECEIVE); - globalBroadcast.putExtra(BluetoothDevice.EXTRA_DEVICE, getBluetoothDevice()); - globalBroadcast.putExtra(Intent.EXTRA_TEXT, data); - sendBroadcast(globalBroadcast); - } + // send the data received to other apps, e.g. the Tasker + final Intent globalBroadcast = new Intent(ACTION_RECEIVE); + globalBroadcast.putExtra(BluetoothDevice.EXTRA_DEVICE, getBluetoothDevice()); + globalBroadcast.putExtra(Intent.EXTRA_TEXT, data); + sendBroadcast(globalBroadcast); + } - @Override - public void onDataSent(final BluetoothDevice device, final String data) { - final Intent broadcast = new Intent(BROADCAST_UART_TX); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, data); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } + @Override + public void onDataSent(final BluetoothDevice device, final String data) { + final Intent broadcast = new Intent(BROADCAST_UART_TX); + broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); + broadcast.putExtra(EXTRA_DATA, data); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } - /** - * Sends the given message to all connected wearables. If the path is equal to {@link Constants.UART#DEVICE_DISCONNECTED} the service will be stopped afterwards. - * @param path message path - * @param message the message - */ - private void sendMessageToWearables(final @NonNull String path, final @NonNull String message) { - if(mGoogleApiClient.isConnected()) { - new Thread(() -> { - NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await(); - for(Node node : nodes.getNodes()) { - Logger.v(getLogSession(), "[WEAR] Sending message '" + path + "' to " + node.getDisplayName()); - final MessageApi.SendMessageResult result = Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), path, message.getBytes()).await(); - if(result.getStatus().isSuccess()){ - Logger.i(getLogSession(), "[WEAR] Message sent"); - } else { - Logger.w(getLogSession(), "[WEAR] Sending message failed: " + result.getStatus().getStatusMessage()); - Log.w(TAG, "Failed to send " + path + " to " + node.getDisplayName()); - } - } - if (Constants.UART.DEVICE_DISCONNECTED.equals(path)) - stopService(); - }).start(); - } else { - if (Constants.UART.DEVICE_DISCONNECTED.equals(path)) - stopService(); - } - } + /** + * Sends the given message to all connected wearables. If the path is equal to {@link Constants.UART#DEVICE_DISCONNECTED} the service will be stopped afterwards. + * + * @param path message path + * @param message the message + */ + private void sendMessageToWearables(final @NonNull String path, final @NonNull String message) { + if (mGoogleApiClient.isConnected()) { + new Thread(() -> { + NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await(); + for (Node node : nodes.getNodes()) { + Logger.v(getLogSession(), "[WEAR] Sending message '" + path + "' to " + node.getDisplayName()); + final MessageApi.SendMessageResult result = Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), path, message.getBytes()).await(); + if (result.getStatus().isSuccess()) { + Logger.i(getLogSession(), "[WEAR] Message sent"); + } else { + Logger.w(getLogSession(), "[WEAR] Sending message failed: " + result.getStatus().getStatusMessage()); + Log.w(TAG, "Failed to send " + path + " to " + node.getDisplayName()); + } + } + if (Constants.UART.DEVICE_DISCONNECTED.equals(path)) + stopService(); + }).start(); + } else { + if (Constants.UART.DEVICE_DISCONNECTED.equals(path)) + stopService(); + } + } - /** - * Creates the notification - * - * @param messageResId - * message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - * signals that will be used to notify the user - */ - private void createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, UARTActivity.class); + /** + * Sets the service as a foreground service + */ + private void 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 + final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification); + } else { + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + } - final Intent disconnect = new Intent(ACTION_DISCONNECT); - disconnect.putExtra(EXTRA_SOURCE, SOURCE_NOTIFICATION); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + /** + * Stops the service as a foreground service + */ + private void 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(); + } + } - // 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_uart); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.uart_notification_action_disconnect), disconnectAction)); + /** + * Creates the notification + * + * @param messageResId message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults signals that will be used to notify the user + */ + protected Notification createNotification(final int messageResId, final int defaults) { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, UARTActivity.class); - final Notification notification = builder.build(); - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } + final Intent disconnect = new Intent(ACTION_DISCONNECT); + disconnect.putExtra(EXTRA_SOURCE, SOURCE_NOTIFICATION); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } + // 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_uart); + builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.uart_notification_action_disconnect), disconnectAction)); - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final int source = intent.getIntExtra(EXTRA_SOURCE, SOURCE_NOTIFICATION); - switch (source) { - case SOURCE_NOTIFICATION: - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - break; - case SOURCE_WEARABLE: - Logger.i(getLogSession(), "[WEAR] '" + Constants.ACTION_DISCONNECT + "' message received"); - break; - } - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; + return builder.build(); + } - /** - * Broadcast receiver that listens for {@link #ACTION_SEND} from other apps. Sends the String or int content of the {@link Intent#EXTRA_TEXT} extra to the remote device. - * The integer content will be sent as String (65 -> "65", not 65 -> "A"). - */ - private BroadcastReceiver mIntentBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final boolean hasMessage = intent.hasExtra(Intent.EXTRA_TEXT); - if (hasMessage) { - String message = intent.getStringExtra(Intent.EXTRA_TEXT); - if (message == null) { - final int intValue = intent.getIntExtra(Intent.EXTRA_TEXT, Integer.MIN_VALUE); // how big is the chance of such data? - if (intValue != Integer.MIN_VALUE) - message = String.valueOf(intValue); - } + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotification() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + } - if (message != null) { - final int source = intent.getIntExtra(EXTRA_SOURCE, SOURCE_3RD_PARTY); - switch (source) { - case SOURCE_WEARABLE: - Logger.i(getLogSession(), "[WEAR] '" + Constants.UART.COMMAND + "' message received with data: \"" + message + "\""); - break; - case SOURCE_3RD_PARTY: - default: - Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received with data: \"" + message + "\""); - break; - } - mManager.send(message); - return; - } - } - // No data od incompatible type of EXTRA_TEXT - if (!hasMessage) - Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received no data."); - else - Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received incompatible data type. Only String and int are supported."); - } - }; + /** + * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. + */ + private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final int source = intent.getIntExtra(EXTRA_SOURCE, SOURCE_NOTIFICATION); + switch (source) { + case SOURCE_NOTIFICATION: + Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); + break; + case SOURCE_WEARABLE: + Logger.i(getLogSession(), "[WEAR] '" + Constants.ACTION_DISCONNECT + "' message received"); + break; + } + if (isConnected()) + getBinder().disconnect(); + else + stopSelf(); + } + }; + + /** + * Broadcast receiver that listens for {@link #ACTION_SEND} from other apps. Sends the String or int content of the {@link Intent#EXTRA_TEXT} extra to the remote device. + * The integer content will be sent as String (65 -> "65", not 65 -> "A"). + */ + private BroadcastReceiver mIntentBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final boolean hasMessage = intent.hasExtra(Intent.EXTRA_TEXT); + if (hasMessage) { + String message = intent.getStringExtra(Intent.EXTRA_TEXT); + if (message == null) { + final int intValue = intent.getIntExtra(Intent.EXTRA_TEXT, Integer.MIN_VALUE); // how big is the chance of such data? + if (intValue != Integer.MIN_VALUE) + message = String.valueOf(intValue); + } + + if (message != null) { + final int source = intent.getIntExtra(EXTRA_SOURCE, SOURCE_3RD_PARTY); + switch (source) { + case SOURCE_WEARABLE: + Logger.i(getLogSession(), "[WEAR] '" + Constants.UART.COMMAND + "' message received with data: \"" + message + "\""); + break; + case SOURCE_3RD_PARTY: + default: + Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received with data: \"" + message + "\""); + break; + } + mManager.send(message); + return; + } + } + // No data od incompatible type of EXTRA_TEXT + if (!hasMessage) + Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received no data."); + else + Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received incompatible data type. Only String and int are supported."); + } + }; }