From 01180f161780c77150da389c32a0277650e49267 Mon Sep 17 00:00:00 2001 From: "Rajaratnam, Roshan" Date: Wed, 7 Aug 2019 14:15:33 +0200 Subject: [PATCH] start/stop service as foreground service for devices running Oreo and above --- .../nrftoolbox/profile/BleProfileService.java | 1118 +++++++++-------- .../proximity/ProximityService.java | 1058 ++++++++-------- 2 files changed, 1102 insertions(+), 1074 deletions(-) diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java index e63c3777..999b1321 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java @@ -33,12 +33,12 @@ import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import no.nordicsemi.android.ble.BleManager; import no.nordicsemi.android.ble.BleManagerCallbacks; import no.nordicsemi.android.ble.utils.ILogger; @@ -47,557 +47,563 @@ import no.nordicsemi.android.log.Logger; @SuppressWarnings("unused") public abstract class BleProfileService extends Service implements BleManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "BleProfileService"; - - public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE"; - public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED"; - public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY"; - public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE"; - @Deprecated - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR"; - - /** The parameter passed when creating the service. Must contain the address of the sensor that we want to connect to */ - public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS"; - /** The key for the device name that is returned in {@link #BROADCAST_CONNECTION_STATE} with state {@link #STATE_CONNECTED}. */ - public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME"; - public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"; - public static final String EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI"; - public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE"; - public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE"; - public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY"; - public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY"; - @Deprecated - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE"; - public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE"; - - public static final int STATE_LINK_LOSS = -1; - public static final int STATE_DISCONNECTED = 0; - public static final int STATE_CONNECTED = 1; - public static final int STATE_CONNECTING = 2; - public static final int STATE_DISCONNECTING = 3; - - private LoggableBleManager mBleManager; - private Handler mHandler; - - protected boolean mBound; - private boolean mActivityIsChangingConfiguration; - private BluetoothDevice mBluetoothDevice; - private String mDeviceName; - private ILogSession mLogSession; - - private final BroadcastReceiver mBluetoothStateBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); - final ILogger logger = getBinder(); - - final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(state); - logger.log(Log.DEBUG, stateString); - - switch (state) { - case BluetoothAdapter.STATE_ON: - onBluetoothEnabled(); - break; - case BluetoothAdapter.STATE_TURNING_OFF: - case BluetoothAdapter.STATE_OFF: - onBluetoothDisabled(); - break; - } - } - - private String state2String(final int state) { - switch (state) { - case BluetoothAdapter.STATE_TURNING_ON: - return "TURNING ON"; - case BluetoothAdapter.STATE_ON: - return "ON"; - case BluetoothAdapter.STATE_TURNING_OFF: - return "TURNING OFF"; - case BluetoothAdapter.STATE_OFF: - return "OFF"; - default: - return "UNKNOWN (" + state + ")"; - } - } - }; - - public class LocalBinder extends Binder implements ILogger { - /** - * Disconnects from the sensor. - */ - public final void disconnect() { - final int state = mBleManager.getConnectionState(); - if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) { - mBleManager.close(); - onDeviceDisconnected(mBluetoothDevice); - return; - } - - mBleManager.disconnect().enqueue(); - } - - /** - * Sets whether the bound activity if changing configuration or not. - * If false, we will turn off battery level notifications in onUnbind(..) method below. - * @param changing true if the bound activity is finishing - */ - public void setActivityIsChangingConfiguration(final boolean changing) { - mActivityIsChangingConfiguration = changing; - } - - /** - * Returns the device address - * - * @return device address - */ - public String getDeviceAddress() { - return mBluetoothDevice.getAddress(); - } - - /** - * Returns the device name - * - * @return the device name - */ - public String getDeviceName() { - return mDeviceName; - } - - /** - * Returns the Bluetooth device - * - * @return the Bluetooth device - */ - public BluetoothDevice getBluetoothDevice() { - return mBluetoothDevice; - } - - /** - * Returns true if the device is connected to the sensor. - * - * @return true if device is connected to the sensor, false otherwise - */ - public boolean isConnected() { - return mBleManager.isConnected(); - } - - - /** - * Returns the connection state of given device. - * @return the connection state, as in {@link BleManager#getConnectionState()}. - */ - public int getConnectionState() { - return mBleManager.getConnectionState(); - } - - /** - * Returns the log session that can be used to append log entries. - * The log session is created when the service is being created. - * The method returns null if the nRF Logger app was not installed. - * - * @return the log session - */ - public ILogSession getLogSession() { - return mLogSession; - } - - @Override - public void log(final int level, @NonNull final String message) { - Logger.log(mLogSession, level, message); - } - - @Override - public void log(final int level, final @StringRes int messageRes, final Object... params) { - Logger.log(mLogSession, level, messageRes, params); - } - } - - /** - * Returns a handler that is created in onCreate(). - * The handler may be used to postpone execution of some operations or to run them in UI thread. - */ - protected Handler getHandler() { - return mHandler; - } - - /** - * Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity. - * - * @return the service binder - */ - protected LocalBinder getBinder() { - // default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation - return new LocalBinder(); - } - - @Override - public IBinder onBind(final Intent intent) { - mBound = true; - return getBinder(); - } - - @Override - public final void onRebind(final Intent intent) { - mBound = true; - - if (!mActivityIsChangingConfiguration) - onRebind(); - } - - /** - * Called when the activity has rebound to the service after being recreated. - * This method is not called when the activity was killed to be recreated when the phone orientation changed - * if prior to being killed called {@link BleProfileService.LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true. - */ - protected void onRebind() { - // empty default implementation - } - - @Override - public final boolean onUnbind(final Intent intent) { - mBound = false; - - if (!mActivityIsChangingConfiguration) - onUnbind(); - - // We want the onRebind method be called if anything else binds to it again - return true; - } - - /** - * Called when the activity has unbound from the service before being finished. - * This method is not called when the activity is killed to be recreated when the phone orientation changed. - */ - protected void onUnbind() { - // empty default implementation - } - - @SuppressWarnings("unchecked") - @Override - public void onCreate() { - super.onCreate(); - - mHandler = new Handler(); - - // Initialize the manager - mBleManager = initializeManager(); - mBleManager.setGattCallbacks(this); - - // Register broadcast receivers - registerReceiver(mBluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - - // Service has now been created - onServiceCreated(); - - // Call onBluetoothEnabled if Bluetooth enabled - final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (bluetoothAdapter.isEnabled()) { - onBluetoothEnabled(); - } - } - - /** - * Called when the service has been created, before the {@link #onBluetoothEnabled()} is called. - */ - protected void onServiceCreated() { - // empty default implementation - } - - /** - * Initializes the Ble Manager responsible for connecting to a single device. - * @return a new BleManager object - */ - @SuppressWarnings("rawtypes") - protected abstract LoggableBleManager initializeManager(); - - /** - * This method returns whether autoConnect option should be used. - * - * @return true to use autoConnect feature, false (default) otherwise. - */ - protected boolean shouldAutoConnect() { - return false; - } - - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS)) - throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key"); - - final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI); - mLogSession = Logger.openSession(getApplicationContext(), logUri); - mDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME); - - Logger.i(mLogSession, "Service started"); - - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); - mBluetoothDevice = adapter.getRemoteDevice(deviceAddress); - - mBleManager.setLogger(mLogSession); - onServiceStarted(); - mBleManager.connect(mBluetoothDevice) - .useAutoConnect(shouldAutoConnect()) - .retry(3, 100) - .enqueue(); - return START_REDELIVER_INTENT; - } - - /** - * Called when the service has been started. The device name and address are set. - * The BLE Manager will try to connect to the device after this method finishes. - */ - protected void onServiceStarted() { - // empty default implementation - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - // This method is called when user removed the app from Recents. - // By default, the service will be killed and recreated immediately after that. - // However, all managed devices will be lost and devices will be disconnected. - stopSelf(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - // Unregister broadcast receivers - unregisterReceiver(mBluetoothStateBroadcastReceiver); - - // shutdown the manager - mBleManager.close(); - Logger.i(mLogSession, "Service destroyed"); - mBleManager = null; - mBluetoothDevice = null; - mDeviceName = null; - mLogSession = null; - mHandler = null; - } - - /** - * Method called when Bluetooth Adapter has been disabled. - */ - protected void onBluetoothDisabled() { - // empty default implementation - } - - /** - * This method is called when Bluetooth Adapter has been enabled and - * after the service was created if Bluetooth Adapter was enabled at that moment. - * This method could initialize all Bluetooth related features, for example open the GATT server. - */ - protected void onBluetoothEnabled() { - // empty default implementation - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_DEVICE_NAME, mDeviceName); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - // Notify user about changing the state to DISCONNECTING - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * This method should return false if the service needs to do some asynchronous work after if has disconnected from the device. - * In that case the {@link #stopService()} method must be called when done. - * @return true (default) to automatically stop the service when device is disconnected. False otherwise. - */ - protected boolean stopWhenDisconnected() { - return true; - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - // Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above - - // Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be - // invoked ONLY when user requested disconnection (using Disconnect button). If the device - // disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead. - - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - if (stopWhenDisconnected()) - stopService(); - } - - protected void stopService() { - // user requested disconnection. We must stop the service - Logger.v(mLogSession, "Stopping service..."); - stopSelf(); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_DEVICE_READY); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - // no need for disconnecting, it will be disconnected by the manager automatically - } - - @Override - public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonded); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding_failed); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - final Intent broadcast = new Intent(BROADCAST_ERROR); - broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); - broadcast.putExtra(EXTRA_ERROR_MESSAGE, message); - broadcast.putExtra(EXTRA_ERROR_CODE, errorCode); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId - * an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - mHandler.post(() -> Toast.makeText(BleProfileService.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message - * a message to be shown - */ - protected void showToast(final String message) { - mHandler.post(() -> Toast.makeText(BleProfileService.this, message, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns the log session that can be used to append log entries. The method returns null if the nRF Logger app was not installed. It is safe to use logger when - * {@link #onServiceStarted()} has been called. - * - * @return the log session - */ - protected ILogSession getLogSession() { - return mLogSession; - } - - /** - * Returns the device address - * - * @return device address - */ - protected String getDeviceAddress() { - return mBluetoothDevice.getAddress(); - } - - /** - * Returns the Bluetooth device object - * - * @return bluetooth device - */ - protected BluetoothDevice getBluetoothDevice() { - return mBluetoothDevice; - } - - /** - * Returns the device name - * - * @return the device name - */ - protected String getDeviceName() { - return mDeviceName; - } - - /** - * Returns true if the device is connected to the sensor. - * - * @return true if device is connected to the sensor, false otherwise - */ - protected boolean isConnected() { - return mBleManager != null && mBleManager.isConnected(); - } + @SuppressWarnings("unused") + private static final String TAG = "BleProfileService"; + + public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE"; + public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED"; + public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY"; + public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE"; + @Deprecated + public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; + public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR"; + + /** + * The parameter passed when creating the service. Must contain the address of the sensor that we want to connect to + */ + public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS"; + /** + * The key for the device name that is returned in {@link #BROADCAST_CONNECTION_STATE} with state {@link #STATE_CONNECTED}. + */ + public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME"; + public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"; + public static final String EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI"; + public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE"; + public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE"; + public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY"; + public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY"; + @Deprecated + public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; + public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE"; + public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE"; + + public static final int STATE_LINK_LOSS = -1; + public static final int STATE_DISCONNECTED = 0; + public static final int STATE_CONNECTED = 1; + public static final int STATE_CONNECTING = 2; + public static final int STATE_DISCONNECTING = 3; + + private LoggableBleManager mBleManager; + private Handler mHandler; + + protected boolean mBound; + private boolean mActivityIsChangingConfiguration; + private BluetoothDevice mBluetoothDevice; + private String mDeviceName; + private ILogSession mLogSession; + + private final BroadcastReceiver mBluetoothStateBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); + final ILogger logger = getBinder(); + + final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(state); + logger.log(Log.DEBUG, stateString); + + switch (state) { + case BluetoothAdapter.STATE_ON: + onBluetoothEnabled(); + break; + case BluetoothAdapter.STATE_TURNING_OFF: + case BluetoothAdapter.STATE_OFF: + onBluetoothDisabled(); + break; + } + } + + private String state2String(final int state) { + switch (state) { + case BluetoothAdapter.STATE_TURNING_ON: + return "TURNING ON"; + case BluetoothAdapter.STATE_ON: + return "ON"; + case BluetoothAdapter.STATE_TURNING_OFF: + return "TURNING OFF"; + case BluetoothAdapter.STATE_OFF: + return "OFF"; + default: + return "UNKNOWN (" + state + ")"; + } + } + }; + + public class LocalBinder extends Binder implements ILogger { + /** + * Disconnects from the sensor. + */ + public final void disconnect() { + final int state = mBleManager.getConnectionState(); + if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) { + mBleManager.close(); + onDeviceDisconnected(mBluetoothDevice); + return; + } + + mBleManager.disconnect().enqueue(); + } + + /** + * Sets whether the bound activity if changing configuration or not. + * If false, we will turn off battery level notifications in onUnbind(..) method below. + * + * @param changing true if the bound activity is finishing + */ + public void setActivityIsChangingConfiguration(final boolean changing) { + mActivityIsChangingConfiguration = changing; + } + + /** + * Returns the device address + * + * @return device address + */ + public String getDeviceAddress() { + return mBluetoothDevice.getAddress(); + } + + /** + * Returns the device name + * + * @return the device name + */ + public String getDeviceName() { + return mDeviceName; + } + + /** + * Returns the Bluetooth device + * + * @return the Bluetooth device + */ + public BluetoothDevice getBluetoothDevice() { + return mBluetoothDevice; + } + + /** + * Returns true if the device is connected to the sensor. + * + * @return true if device is connected to the sensor, false otherwise + */ + public boolean isConnected() { + return mBleManager.isConnected(); + } + + + /** + * Returns the connection state of given device. + * + * @return the connection state, as in {@link BleManager#getConnectionState()}. + */ + public int getConnectionState() { + return mBleManager.getConnectionState(); + } + + /** + * Returns the log session that can be used to append log entries. + * The log session is created when the service is being created. + * The method returns null if the nRF Logger app was not installed. + * + * @return the log session + */ + public ILogSession getLogSession() { + return mLogSession; + } + + @Override + public void log(final int level, @NonNull final String message) { + Logger.log(mLogSession, level, message); + } + + @Override + public void log(final int level, final @StringRes int messageRes, final Object... params) { + Logger.log(mLogSession, level, messageRes, params); + } + } + + /** + * Returns a handler that is created in onCreate(). + * The handler may be used to postpone execution of some operations or to run them in UI thread. + */ + protected Handler getHandler() { + return mHandler; + } + + /** + * Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity. + * + * @return the service binder + */ + protected LocalBinder getBinder() { + // default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation + return new LocalBinder(); + } + + @Override + public IBinder onBind(final Intent intent) { + mBound = true; + return getBinder(); + } + + @Override + public final void onRebind(final Intent intent) { + mBound = true; + + if (!mActivityIsChangingConfiguration) + onRebind(); + } + + /** + * Called when the activity has rebound to the service after being recreated. + * This method is not called when the activity was killed to be recreated when the phone orientation changed + * if prior to being killed called {@link BleProfileService.LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true. + */ + protected void onRebind() { + // empty default implementation + } + + @Override + public final boolean onUnbind(final Intent intent) { + mBound = false; + + if (!mActivityIsChangingConfiguration) + onUnbind(); + + // We want the onRebind method be called if anything else binds to it again + return true; + } + + /** + * Called when the activity has unbound from the service before being finished. + * This method is not called when the activity is killed to be recreated when the phone orientation changed. + */ + protected void onUnbind() { + // empty default implementation + } + + @SuppressWarnings("unchecked") + @Override + public void onCreate() { + super.onCreate(); + + mHandler = new Handler(); + + // Initialize the manager + mBleManager = initializeManager(); + mBleManager.setGattCallbacks(this); + + // Register broadcast receivers + registerReceiver(mBluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + + // Service has now been created + onServiceCreated(); + + // Call onBluetoothEnabled if Bluetooth enabled + final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter.isEnabled()) { + onBluetoothEnabled(); + } + } + + /** + * Called when the service has been created, before the {@link #onBluetoothEnabled()} is called. + */ + protected void onServiceCreated() { + // empty default implementation + } + + /** + * Initializes the Ble Manager responsible for connecting to a single device. + * + * @return a new BleManager object + */ + @SuppressWarnings("rawtypes") + protected abstract LoggableBleManager initializeManager(); + + /** + * This method returns whether autoConnect option should be used. + * + * @return true to use autoConnect feature, false (default) otherwise. + */ + protected boolean shouldAutoConnect() { + return false; + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS)) + throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key"); + + final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI); + mLogSession = Logger.openSession(getApplicationContext(), logUri); + mDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME); + + Logger.i(mLogSession, "Service started"); + + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); + mBluetoothDevice = adapter.getRemoteDevice(deviceAddress); + + mBleManager.setLogger(mLogSession); + onServiceStarted(); + mBleManager.connect(mBluetoothDevice) + .useAutoConnect(shouldAutoConnect()) + .retry(3, 100) + .enqueue(); + return START_REDELIVER_INTENT; + } + + /** + * Called when the service has been started. The device name and address are set. + * The BLE Manager will try to connect to the device after this method finishes. + */ + protected void onServiceStarted() { + // empty default implementation + } + + @Override + public void onTaskRemoved(final Intent rootIntent) { + super.onTaskRemoved(rootIntent); + // This method is called when user removed the app from Recents. + // By default, the service will be killed and recreated immediately after that. + // However, all managed devices will be lost and devices will be disconnected. + stopSelf(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + // Unregister broadcast receivers + unregisterReceiver(mBluetoothStateBroadcastReceiver); + + // shutdown the manager + mBleManager.close(); + Logger.i(mLogSession, "Service destroyed"); + mBleManager = null; + mBluetoothDevice = null; + mDeviceName = null; + mLogSession = null; + mHandler = null; + } + + /** + * Method called when Bluetooth Adapter has been disabled. + */ + protected void onBluetoothDisabled() { + // empty default implementation + } + + /** + * This method is called when Bluetooth Adapter has been enabled and + * after the service was created if Bluetooth Adapter was enabled at that moment. + * This method could initialize all Bluetooth related features, for example open the GATT server. + */ + protected void onBluetoothEnabled() { + // empty default implementation + } + + @Override + public void onDeviceConnecting(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceConnected(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_DEVICE_NAME, mDeviceName); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { + // Notify user about changing the state to DISCONNECTING + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + /** + * This method should return false if the service needs to do some asynchronous work after if has disconnected from the device. + * In that case the {@link #stopService()} method must be called when done. + * + * @return true (default) to automatically stop the service when device is disconnected. False otherwise. + */ + protected boolean stopWhenDisconnected() { + return true; + } + + @Override + public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { + // Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above + + // Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be + // invoked ONLY when user requested disconnection (using Disconnect button). If the device + // disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead. + + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + + if (stopWhenDisconnected()) + stopService(); + } + + protected void stopService() { + // user requested disconnection. We must stop the service + Logger.v(mLogSession, "Stopping service..."); + stopSelf(); + } + + @Override + public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { + final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true); + broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceReady(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_DEVICE_READY); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false); + broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + + // no need for disconnecting, it will be disconnected by the manager automatically + } + + @Override + public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) { + final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onBondingRequired(@NonNull final BluetoothDevice device) { + showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding); + + final Intent broadcast = new Intent(BROADCAST_BOND_STATE); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onBonded(@NonNull final BluetoothDevice device) { + showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonded); + + final Intent broadcast = new Intent(BROADCAST_BOND_STATE); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onBondingFailed(@NonNull final BluetoothDevice device) { + showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding_failed); + + final Intent broadcast = new Intent(BROADCAST_BOND_STATE); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { + final Intent broadcast = new Intent(BROADCAST_ERROR); + broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice); + broadcast.putExtra(EXTRA_ERROR_MESSAGE, message); + broadcast.putExtra(EXTRA_ERROR_CODE, errorCode); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + /** + * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread + * + * @param messageResId an resource id of the message to be shown + */ + protected void showToast(final int messageResId) { + mHandler.post(() -> Toast.makeText(BleProfileService.this, messageResId, Toast.LENGTH_SHORT).show()); + } + + /** + * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread + * + * @param message a message to be shown + */ + protected void showToast(final String message) { + mHandler.post(() -> Toast.makeText(BleProfileService.this, message, Toast.LENGTH_SHORT).show()); + } + + /** + * Returns the log session that can be used to append log entries. The method returns null if the nRF Logger app was not installed. It is safe to use logger when + * {@link #onServiceStarted()} has been called. + * + * @return the log session + */ + protected ILogSession getLogSession() { + return mLogSession; + } + + /** + * Returns the device address + * + * @return device address + */ + protected String getDeviceAddress() { + return mBluetoothDevice.getAddress(); + } + + /** + * Returns the Bluetooth device object + * + * @return bluetooth device + */ + protected BluetoothDevice getBluetoothDevice() { + return mBluetoothDevice; + } + + /** + * Returns the device name + * + * @return the device name + */ + protected String getDeviceName() { + return mDeviceName; + } + + /** + * Returns true if the device is connected to the sensor. + * + * @return true if device is connected to the sensor, false otherwise + */ + protected boolean isConnected() { + return mBleManager != null && mBleManager.isConnected(); + } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java index b9a195e1..039018c6 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java @@ -34,11 +34,7 @@ import android.media.AudioManager; import android.media.MediaPlayer; import android.media.RingtoneManager; import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.os.Build; import android.text.TextUtils; import android.util.Log; @@ -46,6 +42,11 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import no.nordicsemi.android.log.LogContract; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; import no.nordicsemi.android.nrftoolbox.R; @@ -54,517 +55,538 @@ import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileService; public class ProximityService extends BleMulticonnectProfileService implements ProximityManagerCallbacks, ProximityServerManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "ProximityService"; - - 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_ALARM_SWITCHED = "no.nordicsemi.android.nrftoolbox.BROADCAST_ALARM_SWITCHED"; - public static final String EXTRA_ALARM_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_ALARM_STATE"; - - private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_DISCONNECT"; - private final static String ACTION_FIND = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_FIND"; - private final static String ACTION_SILENT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_SILENT"; - - private final static String PROXIMITY_GROUP_ID = "proximity_connected_tags"; - private final static int NOTIFICATION_ID = 1000; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; - private final static int FIND_REQ = 2; - private final static int SILENT_REQ = 3; - - private final ProximityBinder mBinder = new ProximityBinder(); - private ProximityServerManager mServerManager; - private MediaPlayer mMediaPlayer; - private int mOriginalVolume; - /** - * When a device starts an alarm on the phone it is added to this list. - * Alarm is disabled when this list is empty. - */ - private List mDevicesWithAlarm; - - private int mAttempt; - private final static int MAX_ATTEMPTS = 1; - - /** - * This local binder is an interface for the bonded activity to operate with the proximity - * sensor. - */ - public class ProximityBinder extends LocalBinder { - /** - * Toggles the Immediate Alert on given remote device. - * - * @param device the connected device. - */ - public void toggleImmediateAlert(final BluetoothDevice device) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - manager.toggleImmediateAlert(); - } - - /** - * Returns the current alarm state on given device. This value is not read from the device, - * it's just the last value written to it (initially false). - * - * @param device the connected device. - * @return True if alarm has been enabled, false if disabled. - */ - public boolean isImmediateAlertOn(final BluetoothDevice device) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - return manager.isAlertEnabled(); - } - - /** - * Returns the last received battery level value. - * - * @param device the device of which battery level should be returned. - * @return Battery value or null if no value was received or Battery Level characteristic - * was not found, or the device is disconnected. - */ - public Integer getBatteryLevel(final BluetoothDevice device) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - return manager.getBatteryLevel(); - } - } - - @Override - protected LocalBinder getBinder() { - return mBinder; - } - - @Override - protected LoggableBleManager initializeManager() { - return new ProximityManager(this); - } - - /** - * 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 BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); - mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] DISCONNECT action pressed"); - mBinder.disconnect(device); - } - }; - - /** - * This broadcast receiver listens for {@link #ACTION_FIND} or {@link #ACTION_SILENT} that may - * be fired by pressing Find me action button on the notification. - */ - private final BroadcastReceiver mToggleAlarmActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); - switch (intent.getAction()) { - case ACTION_FIND: - mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] FIND action pressed"); - break; - case ACTION_SILENT: - mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] SILENT action pressed"); - break; - } - mBinder.toggleImmediateAlert(device); - } - }; - - @Override - protected void onServiceCreated() { - mServerManager = new ProximityServerManager(this); - mServerManager.setLogger(mBinder); - - initializeAlarm(); - - registerReceiver(mDisconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT)); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_FIND); - filter.addAction(ACTION_SILENT); - registerReceiver(mToggleAlarmActionBroadcastReceiver, filter); - } - - @Override - public void onServiceStopped() { - cancelNotifications(); - - // Close the GATT server. If it hasn't been opened this method does nothing - mServerManager.closeGattServer(); - - releaseAlarm(); - - unregisterReceiver(mDisconnectActionBroadcastReceiver); - unregisterReceiver(mToggleAlarmActionBroadcastReceiver); - - super.onServiceStopped(); - } - - @Override - protected void onBluetoothEnabled() { - mAttempt = 0; - getHandler().post(new Runnable() { - @Override - public void run() { - final Runnable that = this; - // Start the GATT Server only if Bluetooth is enabled - mServerManager.openGattServer(ProximityService.this, - new ProximityServerManager.OnServerOpenCallback() { - @Override - public void onGattServerOpen() { - // We are now ready to reconnect devices - ProximityService.super.onBluetoothEnabled(); - } - - @Override - public void onGattServerFailed(final int error) { - mServerManager.closeGattServer(); - - if (mAttempt < MAX_ATTEMPTS) { - mAttempt++; - getHandler().postDelayed(that, 2000); - } else { - showToast(getString(R.string.proximity_server_error, error)); - // GATT server failed to start, but we may connect as a client - ProximityService.super.onBluetoothEnabled(); - } - } - }); - } - }); - } - - @Override - protected void onBluetoothDisabled() { - super.onBluetoothDisabled(); - // Close the GATT server - mServerManager.closeGattServer(); - } - - @Override - protected void onRebind() { - // When the activity rebinds to the service, remove the notification - cancelNotifications(); - - // This method will read the Battery Level value from each connected device, 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. - for (final BluetoothDevice device : getManagedDevices()) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - manager.readBatteryLevelCharacteristic(); - manager.enableBatteryLevelCharacteristicNotifications(); - } - } - - @Override - public 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. - for (final BluetoothDevice device : getManagedDevices()) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - manager.disableBatteryLevelCharacteristicNotifications(); - } - - createBackgroundNotification(); - } - - @Override - public void onDeviceConnected(final BluetoothDevice device) { - super.onDeviceConnected(device); - - if (!mBound) { - createBackgroundNotification(); - } - } - - @Override - public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) { - super.onServicesDiscovered(device, optionalServicesFound); - mServerManager.openConnection(device); - } - - @Override - public void onLinkLossOccurred(final BluetoothDevice device) { - mServerManager.cancelConnection(device); - stopAlarm(device); - super.onLinkLossOccurred(device); - - if (!mBound) { - createBackgroundNotification(); - if (BluetoothAdapter.getDefaultAdapter().isEnabled()) - createLinkLossNotification(device); - else - cancelNotification(device); - } - } - - @Override - public void onDeviceDisconnected(final BluetoothDevice device) { - mServerManager.cancelConnection(device); - stopAlarm(device); - super.onDeviceDisconnected(device); - - if (!mBound) { - cancelNotification(device); - createBackgroundNotification(); - } - } - - @Override - public void onAlarmTriggered(@NonNull final BluetoothDevice device) { - playAlarm(device); - } - - @Override - public void onAlarmStopped(@NonNull final BluetoothDevice device) { - stopAlarm(device); - } - - @Override - public void onRemoteAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on) { - final Intent broadcast = new Intent(BROADCAST_ALARM_SWITCHED); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_ALARM_STATE, on); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - if (!mBound) { - createBackgroundNotification(); - } - } - - @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); - } - - private void createBackgroundNotification() { - final List connectedDevices = getConnectedDevices(); - for (final BluetoothDevice device : connectedDevices) { - createNotificationForConnectedDevice(device); - } - createSummaryNotification(); - } - - private void createSummaryNotification() { - final NotificationCompat.Builder builder = getNotificationBuilder(); - builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); - builder.setShowWhen(false).setDefaults(0); - // An ongoing notification will not be shown on Android Wear. - builder.setOngoing(true); - builder.setGroup(PROXIMITY_GROUP_ID).setGroupSummary(true); - builder.setContentTitle(getString(R.string.app_name)); - - final List managedDevices = getManagedDevices(); - final List connectedDevices = getConnectedDevices(); - if (connectedDevices.isEmpty()) { - // No connected devices - final int numberOfManagedDevices = managedDevices.size(); - if (numberOfManagedDevices == 1) { - final String name = getDeviceName(managedDevices.get(0)); - // We don't use plurals here, as we only have the default language and 'one' is not - // in every language (versions differ in %d or %s) and throw an exception in e.g. in Chinese. - builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name)); - } else { - builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfManagedDevices)); - } - } else { - // There are some proximity tags connected - final StringBuilder text = new StringBuilder(); - - final int numberOfConnectedDevices = connectedDevices.size(); - if (numberOfConnectedDevices == 1) { - final String name = getDeviceName(connectedDevices.get(0)); - text.append(getString(R.string.proximity_notification_summary_text_name, name)); - } else { - text.append(getString(R.string.proximity_notification_summary_text_number, numberOfConnectedDevices)); - } - - // If there are some disconnected devices, also print them - final int numberOfDisconnectedDevices = managedDevices.size() - numberOfConnectedDevices; - if (numberOfDisconnectedDevices == 1) { - text.append(", "); - // Find the single disconnected device to get its name - for (final BluetoothDevice device : managedDevices) { - if (!isConnected(device)) { - final String name = getDeviceName(device); - text.append(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name)); - break; - } - } - } else if (numberOfDisconnectedDevices > 1) { - text.append(", "); - // If there are more, just write number of them - text.append(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfDisconnectedDevices)); - } - text.append("."); - builder.setContentText(text); - } - - final Notification notification = builder.build(); - final NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.notify(NOTIFICATION_ID, notification); - } - - /** - * Creates the notification for given connected device. - * Adds 3 action buttons: DISCONNECT, FIND and SILENT which perform given action on the device. - */ - private void createNotificationForConnectedDevice(final BluetoothDevice device) { - final NotificationCompat.Builder builder = getNotificationBuilder(); - builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); - builder.setGroup(PROXIMITY_GROUP_ID).setDefaults(0); - // An ongoing notification will not be shown on Android Wear. - builder.setOngoing(true); - builder.setContentTitle(getString(R.string.proximity_notification_text, getDeviceName(device))); - - // Add DISCONNECT action - final Intent disconnect = new Intent(ACTION_DISCONNECT); - disconnect.putExtra(EXTRA_DEVICE, device); - final PendingIntent disconnectAction = - PendingIntent.getBroadcast(this, DISCONNECT_REQ + device.hashCode(), - disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.proximity_action_disconnect), disconnectAction)); - // This will keep the same order of notification even after an action was clicked on one of them. - builder.setSortKey(getDeviceName(device) + device.getAddress()); - - // Add FIND or SILENT action - final ProximityManager manager = (ProximityManager) getBleManager(device); - if (manager.isAlertEnabled()) { - final Intent silentAllIntent = new Intent(ACTION_SILENT); - silentAllIntent.putExtra(EXTRA_DEVICE, device); - final PendingIntent silentAction = - PendingIntent.getBroadcast(this, SILENT_REQ + device.hashCode(), - silentAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_silent, getString(R.string.proximity_action_silent), silentAction)); - } else { - final Intent findAllIntent = new Intent(ACTION_FIND); - findAllIntent.putExtra(EXTRA_DEVICE, device); - final PendingIntent findAction = - PendingIntent.getBroadcast(this, FIND_REQ + device.hashCode(), - findAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_find, getString(R.string.proximity_action_find), findAction)); - } - - final Notification notification = builder.build(); - final NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.notify(device.getAddress(), NOTIFICATION_ID, notification); - } - - /** - * Creates a notification showing information about a device that got disconnected. - */ - private void createLinkLossNotification(final BluetoothDevice device) { - final NotificationCompat.Builder builder = getNotificationBuilder(); - builder.setColor(ContextCompat.getColor(this, R.color.orange)); - - final Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); - // Make sure the sound is played even in DND mode - builder.setSound(notificationUri, AudioManager.STREAM_ALARM); - builder.setPriority(NotificationCompat.PRIORITY_HIGH); - builder.setCategory(NotificationCompat.CATEGORY_ALARM); - builder.setShowWhen(true); - // An ongoing notification would not be shown on Android Wear. - builder.setOngoing(false); - // This notification is to be shown not in a group - - final String name = getDeviceName(device); - builder.setContentTitle(getString(R.string.proximity_notification_link_loss_alert, name)); - builder.setTicker(getString(R.string.proximity_notification_link_loss_alert, name)); - - final Notification notification = builder.build(); - final NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.notify(device.getAddress(), NOTIFICATION_ID, notification); - } - - private NotificationCompat.Builder getNotificationBuilder() { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, ProximityActivity.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.PROXIMITY_WARNINGS_CHANNEL); - builder.setContentIntent(pendingIntent).setAutoCancel(false); - builder.setSmallIcon(R.drawable.ic_stat_notify_proximity); - return builder; - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotifications() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - - final List managedDevices = getManagedDevices(); - for (final BluetoothDevice device : managedDevices) { - nm.cancel(device.getAddress(), NOTIFICATION_ID); - } - } - - /** - * Cancels the existing notification for given device. If there is no active notification this method does nothing - */ - private void cancelNotification(final BluetoothDevice device) { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(device.getAddress(), NOTIFICATION_ID); - } - - private void initializeAlarm() { - mDevicesWithAlarm = new LinkedList<>(); - mMediaPlayer = new MediaPlayer(); - mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); - mMediaPlayer.setLooping(true); - mMediaPlayer.setVolume(1.0f, 1.0f); - try { - mMediaPlayer.setDataSource(this, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)); - } catch (final IOException e) { - Log.e(TAG, "Initialize Alarm failed: ", e); - } - } - - private void releaseAlarm() { - mMediaPlayer.release(); - mMediaPlayer = null; - } - - private void playAlarm(final BluetoothDevice device) { - final boolean alarmPlaying = !mDevicesWithAlarm.isEmpty(); - if (!mDevicesWithAlarm.contains(device)) - mDevicesWithAlarm.add(device); - - if (!alarmPlaying) { - // Save the current alarm volume and set it to max - final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - mOriginalVolume = am.getStreamVolume(AudioManager.STREAM_ALARM); - am.setStreamVolume(AudioManager.STREAM_ALARM, am.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); - try { - mMediaPlayer.prepare(); - mMediaPlayer.start(); - } catch (final IOException e) { - Log.e(TAG, "Prepare Alarm failed: ", e); - } - } - } - - private void stopAlarm(final BluetoothDevice device) { - mDevicesWithAlarm.remove(device); - if (mDevicesWithAlarm.isEmpty() && mMediaPlayer.isPlaying()) { - mMediaPlayer.stop(); - // Restore original volume - final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - am.setStreamVolume(AudioManager.STREAM_ALARM, mOriginalVolume, 0); - } - } - - private String getDeviceName(final BluetoothDevice device) { - String name = device.getName(); - if (TextUtils.isEmpty(name)) - name = getString(R.string.proximity_default_device_name); - return name; - } + @SuppressWarnings("unused") + private static final String TAG = "ProximityService"; + + 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_ALARM_SWITCHED = "no.nordicsemi.android.nrftoolbox.BROADCAST_ALARM_SWITCHED"; + public static final String EXTRA_ALARM_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_ALARM_STATE"; + + private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_DISCONNECT"; + private final static String ACTION_FIND = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_FIND"; + private final static String ACTION_SILENT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_SILENT"; + + private final static String PROXIMITY_GROUP_ID = "proximity_connected_tags"; + private final static int NOTIFICATION_ID = 1000; + private final static int OPEN_ACTIVITY_REQ = 0; + private final static int DISCONNECT_REQ = 1; + private final static int FIND_REQ = 2; + private final static int SILENT_REQ = 3; + + private final ProximityBinder mBinder = new ProximityBinder(); + private ProximityServerManager mServerManager; + private MediaPlayer mMediaPlayer; + private int mOriginalVolume; + /** + * When a device starts an alarm on the phone it is added to this list. + * Alarm is disabled when this list is empty. + */ + private List mDevicesWithAlarm; + + private int mAttempt; + private final static int MAX_ATTEMPTS = 1; + + /** + * This local binder is an interface for the bonded activity to operate with the proximity + * sensor. + */ + public class ProximityBinder extends LocalBinder { + /** + * Toggles the Immediate Alert on given remote device. + * + * @param device the connected device. + */ + public void toggleImmediateAlert(final BluetoothDevice device) { + final ProximityManager manager = (ProximityManager) getBleManager(device); + manager.toggleImmediateAlert(); + } + + /** + * Returns the current alarm state on given device. This value is not read from the device, + * it's just the last value written to it (initially false). + * + * @param device the connected device. + * @return True if alarm has been enabled, false if disabled. + */ + public boolean isImmediateAlertOn(final BluetoothDevice device) { + final ProximityManager manager = (ProximityManager) getBleManager(device); + return manager.isAlertEnabled(); + } + + /** + * Returns the last received battery level value. + * + * @param device the device of which battery level should be returned. + * @return Battery value or null if no value was received or Battery Level characteristic + * was not found, or the device is disconnected. + */ + public Integer getBatteryLevel(final BluetoothDevice device) { + final ProximityManager manager = (ProximityManager) getBleManager(device); + return manager.getBatteryLevel(); + } + } + + @Override + protected LocalBinder getBinder() { + return mBinder; + } + + @Override + protected LoggableBleManager initializeManager() { + return new ProximityManager(this); + } + + /** + * 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 BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); + mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] DISCONNECT action pressed"); + mBinder.disconnect(device); + } + }; + + /** + * This broadcast receiver listens for {@link #ACTION_FIND} or {@link #ACTION_SILENT} that may + * be fired by pressing Find me action button on the notification. + */ + private final BroadcastReceiver mToggleAlarmActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); + switch (intent.getAction()) { + case ACTION_FIND: + mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] FIND action pressed"); + break; + case ACTION_SILENT: + mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] SILENT action pressed"); + break; + } + mBinder.toggleImmediateAlert(device); + } + }; + + @Override + protected void onServiceCreated() { + mServerManager = new ProximityServerManager(this); + mServerManager.setLogger(mBinder); + + initializeAlarm(); + + registerReceiver(mDisconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT)); + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_FIND); + filter.addAction(ACTION_SILENT); + registerReceiver(mToggleAlarmActionBroadcastReceiver, filter); + } + + @Override + public void onServiceStopped() { + cancelNotifications(); + + // Close the GATT server. If it hasn't been opened this method does nothing + mServerManager.closeGattServer(); + + releaseAlarm(); + + unregisterReceiver(mDisconnectActionBroadcastReceiver); + unregisterReceiver(mToggleAlarmActionBroadcastReceiver); + + super.onServiceStopped(); + } + + @Override + protected void onBluetoothEnabled() { + mAttempt = 0; + getHandler().post(new Runnable() { + @Override + public void run() { + final Runnable that = this; + // Start the GATT Server only if Bluetooth is enabled + mServerManager.openGattServer(ProximityService.this, + new ProximityServerManager.OnServerOpenCallback() { + @Override + public void onGattServerOpen() { + // We are now ready to reconnect devices + ProximityService.super.onBluetoothEnabled(); + } + + @Override + public void onGattServerFailed(final int error) { + mServerManager.closeGattServer(); + + if (mAttempt < MAX_ATTEMPTS) { + mAttempt++; + getHandler().postDelayed(that, 2000); + } else { + showToast(getString(R.string.proximity_server_error, error)); + // GATT server failed to start, but we may connect as a client + ProximityService.super.onBluetoothEnabled(); + } + } + }); + } + }); + } + + @Override + protected void onBluetoothDisabled() { + super.onBluetoothDisabled(); + // Close the GATT server + mServerManager.closeGattServer(); + } + + @Override + protected void onRebind() { + // When the activity rebinds to the service, remove the notification + cancelNotifications(); + + // This method will read the Battery Level value from each connected device, 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. + for (final BluetoothDevice device : getManagedDevices()) { + final ProximityManager manager = (ProximityManager) getBleManager(device); + manager.readBatteryLevelCharacteristic(); + manager.enableBatteryLevelCharacteristicNotifications(); + } + } + + @Override + public 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. + for (final BluetoothDevice device : getManagedDevices()) { + final ProximityManager manager = (ProximityManager) getBleManager(device); + manager.disableBatteryLevelCharacteristicNotifications(); + } + + createBackgroundNotification(); + } + + @Override + public void onDeviceConnected(final BluetoothDevice device) { + super.onDeviceConnected(device); + + if (!mBound) { + createBackgroundNotification(); + } + } + + @Override + public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) { + super.onServicesDiscovered(device, optionalServicesFound); + mServerManager.openConnection(device); + } + + @Override + public void onLinkLossOccurred(final BluetoothDevice device) { + mServerManager.cancelConnection(device); + stopAlarm(device); + super.onLinkLossOccurred(device); + + if (!mBound) { + createBackgroundNotification(); + if (BluetoothAdapter.getDefaultAdapter().isEnabled()) + createLinkLossNotification(device); + else + cancelNotification(device); + } + } + + @Override + public void onDeviceDisconnected(final BluetoothDevice device) { + mServerManager.cancelConnection(device); + stopAlarm(device); + super.onDeviceDisconnected(device); + + if (!mBound) { + cancelNotification(device); + createBackgroundNotification(); + } + } + + @Override + public void onAlarmTriggered(@NonNull final BluetoothDevice device) { + playAlarm(device); + } + + @Override + public void onAlarmStopped(@NonNull final BluetoothDevice device) { + stopAlarm(device); + } + + @Override + public void onRemoteAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on) { + final Intent broadcast = new Intent(BROADCAST_ALARM_SWITCHED); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_ALARM_STATE, on); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + + if (!mBound) { + createBackgroundNotification(); + } + } + + @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); + } + + private void createBackgroundNotification() { + final List connectedDevices = getConnectedDevices(); + for (final BluetoothDevice device : connectedDevices) { + createNotificationForConnectedDevice(device); + } + createSummaryNotification(); + } + + private void createSummaryNotification() { + final NotificationCompat.Builder builder = getNotificationBuilder(); + builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); + builder.setShowWhen(false).setDefaults(0); + // An ongoing notification will not be shown on Android Wear. + builder.setOngoing(true); + builder.setGroup(PROXIMITY_GROUP_ID).setGroupSummary(true); + builder.setContentTitle(getString(R.string.app_name)); + + final List managedDevices = getManagedDevices(); + final List connectedDevices = getConnectedDevices(); + if (connectedDevices.isEmpty()) { + // No connected devices + final int numberOfManagedDevices = managedDevices.size(); + if (numberOfManagedDevices == 1) { + final String name = getDeviceName(managedDevices.get(0)); + // We don't use plurals here, as we only have the default language and 'one' is not + // in every language (versions differ in %d or %s) and throw an exception in e.g. in Chinese. + builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name)); + } else { + builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfManagedDevices)); + } + } else { + // There are some proximity tags connected + final StringBuilder text = new StringBuilder(); + + final int numberOfConnectedDevices = connectedDevices.size(); + if (numberOfConnectedDevices == 1) { + final String name = getDeviceName(connectedDevices.get(0)); + text.append(getString(R.string.proximity_notification_summary_text_name, name)); + } else { + text.append(getString(R.string.proximity_notification_summary_text_number, numberOfConnectedDevices)); + } + + // If there are some disconnected devices, also print them + final int numberOfDisconnectedDevices = managedDevices.size() - numberOfConnectedDevices; + if (numberOfDisconnectedDevices == 1) { + text.append(", "); + // Find the single disconnected device to get its name + for (final BluetoothDevice device : managedDevices) { + if (!isConnected(device)) { + final String name = getDeviceName(device); + text.append(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name)); + break; + } + } + } else if (numberOfDisconnectedDevices > 1) { + text.append(", "); + // If there are more, just write number of them + text.append(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfDisconnectedDevices)); + } + text.append("."); + builder.setContentText(text); + } + + final Notification notification = builder.build(); + final NotificationManagerCompat nm = NotificationManagerCompat.from(this); + nm.notify(NOTIFICATION_ID, notification); + } + + /** + * Creates the notification for given connected device. + * Adds 3 action buttons: DISCONNECT, FIND and SILENT which perform given action on the device. + */ + private void createNotificationForConnectedDevice(final BluetoothDevice device) { + final NotificationCompat.Builder builder = getNotificationBuilder(); + builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); + builder.setGroup(PROXIMITY_GROUP_ID).setDefaults(0); + // An ongoing notification will not be shown on Android Wear. + builder.setOngoing(true); + builder.setContentTitle(getString(R.string.proximity_notification_text, getDeviceName(device))); + + // Add DISCONNECT action + final Intent disconnect = new Intent(ACTION_DISCONNECT); + disconnect.putExtra(EXTRA_DEVICE, device); + final PendingIntent disconnectAction = + PendingIntent.getBroadcast(this, DISCONNECT_REQ + device.hashCode(), + disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.proximity_action_disconnect), disconnectAction)); + // This will keep the same order of notification even after an action was clicked on one of them. + builder.setSortKey(getDeviceName(device) + device.getAddress()); + + // Add FIND or SILENT action + final ProximityManager manager = (ProximityManager) getBleManager(device); + if (manager.isAlertEnabled()) { + final Intent silentAllIntent = new Intent(ACTION_SILENT); + silentAllIntent.putExtra(EXTRA_DEVICE, device); + final PendingIntent silentAction = + PendingIntent.getBroadcast(this, SILENT_REQ + device.hashCode(), + silentAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_silent, getString(R.string.proximity_action_silent), silentAction)); + } else { + final Intent findAllIntent = new Intent(ACTION_FIND); + findAllIntent.putExtra(EXTRA_DEVICE, device); + final PendingIntent findAction = + PendingIntent.getBroadcast(this, FIND_REQ + device.hashCode(), + findAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_find, getString(R.string.proximity_action_find), findAction)); + } + + final Notification notification = builder.build(); + 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 a notification showing information about a device that got disconnected. + */ + private void createLinkLossNotification(final BluetoothDevice device) { + final NotificationCompat.Builder builder = getNotificationBuilder(); + builder.setColor(ContextCompat.getColor(this, R.color.orange)); + + final Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + // Make sure the sound is played even in DND mode + builder.setSound(notificationUri, AudioManager.STREAM_ALARM); + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setCategory(NotificationCompat.CATEGORY_ALARM); + builder.setShowWhen(true); + // An ongoing notification would not be shown on Android Wear. + builder.setOngoing(false); + // This notification is to be shown not in a group + + final String name = getDeviceName(device); + builder.setContentTitle(getString(R.string.proximity_notification_link_loss_alert, name)); + builder.setTicker(getString(R.string.proximity_notification_link_loss_alert, name)); + + final Notification notification = builder.build(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(NOTIFICATION_ID, notification); + } else { + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(device.getAddress(), NOTIFICATION_ID, notification); + } + } + + private NotificationCompat.Builder getNotificationBuilder() { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, ProximityActivity.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.PROXIMITY_WARNINGS_CHANNEL); + builder.setContentIntent(pendingIntent).setAutoCancel(false); + builder.setSmallIcon(R.drawable.ic_stat_notify_proximity); + return builder; + } + + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotifications() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stopForeground(true); + } else { + nm.cancel(NOTIFICATION_ID); + } + + final List managedDevices = getManagedDevices(); + for (final BluetoothDevice device : managedDevices) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stopForeground(true); + } else { + nm.cancel(device.getAddress(), NOTIFICATION_ID); + } + } + } + + /** + * Cancels the existing notification for given device. If there is no active notification this method does nothing + */ + private void cancelNotification(final BluetoothDevice device) { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stopForeground(true); + } else { + nm.cancel(device.getAddress(), NOTIFICATION_ID); + } + } + + private void initializeAlarm() { + mDevicesWithAlarm = new LinkedList<>(); + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + mMediaPlayer.setLooping(true); + mMediaPlayer.setVolume(1.0f, 1.0f); + try { + mMediaPlayer.setDataSource(this, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)); + } catch (final IOException e) { + Log.e(TAG, "Initialize Alarm failed: ", e); + } + } + + private void releaseAlarm() { + mMediaPlayer.release(); + mMediaPlayer = null; + } + + private void playAlarm(final BluetoothDevice device) { + final boolean alarmPlaying = !mDevicesWithAlarm.isEmpty(); + if (!mDevicesWithAlarm.contains(device)) + mDevicesWithAlarm.add(device); + + if (!alarmPlaying) { + // Save the current alarm volume and set it to max + final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mOriginalVolume = am.getStreamVolume(AudioManager.STREAM_ALARM); + am.setStreamVolume(AudioManager.STREAM_ALARM, am.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); + try { + mMediaPlayer.prepare(); + mMediaPlayer.start(); + } catch (final IOException e) { + Log.e(TAG, "Prepare Alarm failed: ", e); + } + } + } + + private void stopAlarm(final BluetoothDevice device) { + mDevicesWithAlarm.remove(device); + if (mDevicesWithAlarm.isEmpty() && mMediaPlayer.isPlaying()) { + mMediaPlayer.stop(); + // Restore original volume + final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + am.setStreamVolume(AudioManager.STREAM_ALARM, mOriginalVolume, 0); + } + } + + private String getDeviceName(final BluetoothDevice device) { + String name = device.getName(); + if (TextUtils.isEmpty(name)) + name = getString(R.string.proximity_default_device_name); + return name; + } }