diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileService.java new file mode 100644 index 00000000..58db11c6 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileService.java @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrftoolbox.profile.multiconnect; + +import android.app.Service; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.support.annotation.StringRes; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import no.nordicsemi.android.log.ILogSession; +import no.nordicsemi.android.log.LogContract; +import no.nordicsemi.android.nrftoolbox.profile.BleManager; +import no.nordicsemi.android.nrftoolbox.profile.BleManagerCallbacks; +import no.nordicsemi.android.nrftoolbox.profile.ILogger; + +public abstract class BleMulticonnectProfileService extends Service implements BleManagerCallbacks { + @SuppressWarnings("unused") + private static final String TAG = "BleMultiProfileService"; + + 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"; + 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"; + + public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"; + 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"; + 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 HashMap> mBleManagers; + private List mManagedDevices; + private Handler mHandler; + + protected boolean mBinded; + private boolean mActivityIsChangingConfiguration; + + 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(LogContract.Log.Level.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, IDeviceLogger { + /** + * Returns an unmodifiable list of devices managed by the service. + * The returned devices do not need to be connected at tha moment. Each of them was however created + * using {@link #connect(BluetoothDevice)} method so they might have been connected before and disconnected. + * @return unmodifiable list of devices managed by the service + */ + public final List getManagedDevices() { + return Collections.unmodifiableList(mManagedDevices); + } + + /** + * Connects to the given device. If the device is already connected this method does nothing. + * @param device target Bluetooth device + */ + public void connect(final BluetoothDevice device) { + connect(device, null); + } + + /** + * Adds the given device to managed and stars connecting to it. If the device is already connected this method does nothing. + * @param device target Bluetooth device + * @param session log session that has to be used by the device + */ + @SuppressWarnings("unchecked") + public void connect(final BluetoothDevice device, final ILogSession session) { + BleManager manager = mBleManagers.get(device); + if (manager != null) { + if (!manager.isConnected()) + manager.connect(device); + // else do nothing, the device is already connected + } else { + mManagedDevices.add(device); + mBleManagers.put(device, manager = initializeManager()); + manager.setGattCallbacks(BleMulticonnectProfileService.this); + manager.setLogger(session); + manager.connect(device); + } + } + + /** + * Disconnects the given device and removes the associated BleManager object. + * If the list of BleManagers is empty while the last activity unbinds from the service, + * the service will stop itself. + * @param device target device to disconnect and forget + */ + public void disconnect(final BluetoothDevice device) { + final BleManager manager = mBleManagers.get(device); + if (manager != null && manager.isConnected()) { + manager.disconnect(); + } + mManagedDevices.remove(device); + } + + /** + * Returns true if the device is connected to the sensor. + * @param device the target device + * @return true if device is connected to the sensor, false otherwise + */ + public final boolean isConnected(final BluetoothDevice device) { + final BleManager manager = mBleManagers.get(device); + return manager != null && manager.isConnected(); + } + + /** + * Returns the connection state of given device. + * @param device the target device + * @return the connection state, as in {@link BleManager#getConnectionState()}. + */ + public final int getConnectionState(final BluetoothDevice device) { + final BleManager manager = mBleManagers.get(device); + return manager != null ? manager.getConnectionState() : BluetoothGatt.STATE_DISCONNECTED; + } + + /** + * Returns the last received battery level value. + * @param device the device of which battery level should be returned + * @return battery value or -1 if no value was received or Battery Level characteristic was not found + */ + public int getBatteryValue(final BluetoothDevice device) { + final BleManager manager = mBleManagers.get(device); + return manager.getBatteryValue(); + } + + /** + * 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 final void setActivityIsChangingConfiguration(final boolean changing) { + mActivityIsChangingConfiguration = changing; + } + + @Override + public void log(final BluetoothDevice device, final int level, final String message) { + final BleManager manager = mBleManagers.get(device); + if (manager != null) + manager.log(level, message); + } + + @Override + public void log(final BluetoothDevice device, final int level, @StringRes final int messageRes, final Object... params) { + final BleManager manager = mBleManagers.get(device); + if (manager != null) + manager.log(level, messageRes, params); + } + + @Override + public void log(final int level, final String message) { + for (final BleManager manager : mBleManagers.values()) + manager.log(level, message); + } + + @Override + public void log(final int level, @StringRes final int messageRes, final Object... params) { + for (final BleManager manager : mBleManagers.values()) + manager.log(level, messageRes, params); + } + } + + /** + * 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) { + mBinded = true; + return getBinder(); + } + + @Override + public final void onRebind(final Intent intent) { + mBinded = true; + + if (!mActivityIsChangingConfiguration) { + onRebind(); + // 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 BleManager manager : mBleManagers.values()) { + if (manager.isConnected()) + manager.readBatteryLevel(); + } + } + } + + /** + * Called when the activity has rebound to the service after being recreated. + * This method is not called when the activity was killed to be recreated when the phone orientation changed + * if prior to being killed called {@link LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true. + */ + protected void onRebind() { + // empty default implementation + } + + @Override + public final boolean onUnbind(final Intent intent) { + mBinded = false; + + if (!mActivityIsChangingConfiguration) { + if (!mManagedDevices.isEmpty()) { + 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 BleManager manager : mBleManagers.values()) { + if (manager.isConnected()) + manager.setBatteryNotifications(false); + } + } else { + // The last activity has disconnected from the service and there are no devices to manage. The service may be stopped. + stopSelf(); + } + } + + // 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 + } + + @Override + public void onCreate() { + super.onCreate(); + + mHandler = new Handler(); + + // Initialize the map of BLE managers + mBleManagers = new HashMap<>(); + mManagedDevices = new ArrayList<>(); + + // 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 BleManager initializeManager(); + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + onServiceStarted(); + return super.onStartCommand(intent, flags, startId); + } + + /** + * Called when the service has been started. + */ + 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(); + onServiceStopped(); + } + + /** + * Called when the service has been stopped. + */ + protected void onServiceStopped() { + // Unregister broadcast receivers + unregisterReceiver(mBluetoothStateBroadcastReceiver); + + // The managers map may not be empty if the service was killed by the system + for (final BleManager manager : mBleManagers.values()) { + // Service is being destroyed, no need to disconnect manually. + manager.close(); + manager.log(LogContract.Log.Level.INFO, "Service destroyed"); + } + mBleManagers.clear(); + mManagedDevices.clear(); + mBleManagers = null; + mManagedDevices = null; + } + + /** + * Method called when Bluetooth Adapter has been disabled. + */ + protected void onBluetoothDisabled() { + for (final BleManager manager : mBleManagers.values()) { + // Devices were disconnected, no need to disconnect manually. + manager.close(); + } + } + + /** + * This method is called when Bluetooth Adapter has been enabled. It is also called + * 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. + * Make sure you call super.onBluetoothEnabled() at this methods reconnects to + * devices that were connected before the Bluetooth was turned off. + */ + protected void onBluetoothEnabled() { + for (final BluetoothDevice device : mManagedDevices) { + final BleManager manager = mBleManagers.get(device); + if (!manager.isConnected()) + manager.connect(device); + } + } + + @Override + public boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device) { + // By default the Battery Level notifications will be enabled only the activity is bound. + return mBinded; + } + + @Override + public void onDeviceConnecting(final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING); + LocalBroadcastManager.getInstance(BleMulticonnectProfileService.this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceConnected(final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceDisconnecting(final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceDisconnected(final BluetoothDevice device) { + mManagedDevices.remove(device); + // The BleManager is not removed from the HashMap to keep the device's log session. + // mBleManagers.remove(device); + + // Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onLinklossOccur(final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) { + final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true); + broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceReady(final BluetoothDevice device) { + final Intent broadcast = new Intent(BROADCAST_DEVICE_READY); + broadcast.putExtra(EXTRA_DEVICE, device); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDeviceNotSupported(final BluetoothDevice device) { + // We don't like this device, remove it from both collections + mManagedDevices.remove(device); + mBleManagers.remove(device); + + final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); + broadcast.putExtra(EXTRA_DEVICE, device); + 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(final BluetoothDevice device, final int value) { + final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onBondingRequired(final BluetoothDevice device) { + showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding); + + final Intent broadcast = new Intent(BROADCAST_BOND_STATE); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onBonded(final BluetoothDevice device) { + showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonded); + + final Intent broadcast = new Intent(BROADCAST_BOND_STATE); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onError(final BluetoothDevice device, final String message, final int errorCode) { + final Intent broadcast = new Intent(BROADCAST_ERROR); + broadcast.putExtra(EXTRA_DEVICE, device); + broadcast.putExtra(EXTRA_ERROR_MESSAGE, message); + broadcast.putExtra(EXTRA_ERROR_CODE, errorCode); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + + // After receiving an error the device will be automatically disconnected. + // Replace it with other implementation if necessary. + final BleManager manager = mBleManagers.get(device); + manager.disconnect(); + } + + /** + * 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(new Runnable() { + @Override + public void run() { + Toast.makeText(BleMulticonnectProfileService.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(new Runnable() { + @Override + public void run() { + Toast.makeText(BleMulticonnectProfileService.this, message, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * Returns the {@link BleManager} object associated with given device, or null if such has not been created. + * To create a BleManager call the {@link LocalBinder#connect(BluetoothDevice)} method must be called. + * @param device the target device + * @return the BleManager or null + */ + protected BleManager getBleManager(final BluetoothDevice device) { + return mBleManagers.get(device); + } + + /** + * Returns unmodifiable list of all managed devices. They don't have to be connected at the moment. + * @return list of managed devices + */ + protected List getManagedDevices() { + return Collections.unmodifiableList(mManagedDevices); + } + + /** + * Returns a list of those managed devices that are connected at the moment. + * @return list of connected devices + */ + protected List getConnectedDevices() { + final List list = new ArrayList<>(); + for (BluetoothDevice device : mManagedDevices) { + if (mBleManagers.get(device).isConnected()) + list.add(device); + } + return Collections.unmodifiableList(list); + } + + /** + * Returns true if the device is connected to the sensor. + * @param device the target device + * @return true if device is connected to the sensor, false otherwise + */ + protected boolean isConnected(final BluetoothDevice device) { + final BleManager manager = mBleManagers.get(device); + return manager != null && manager.isConnected(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileServiceReadyActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileServiceReadyActivity.java new file mode 100644 index 00000000..a7e6228e --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileServiceReadyActivity.java @@ -0,0 +1,552 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrftoolbox.profile.multiconnect; + +import android.app.Service; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import no.nordicsemi.android.log.ILogSession; +import no.nordicsemi.android.log.LocalLogSession; +import no.nordicsemi.android.log.LogContract; +import no.nordicsemi.android.log.Logger; +import no.nordicsemi.android.nrftoolbox.AppHelpFragment; +import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.profile.BleManagerCallbacks; +import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; +import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; + +/** + *

+ * The {@link BleMulticonnectProfileServiceReadyActivity} activity is designed to be the base class for profile activities that uses services in order to connect + * more than one device at the same time. A service extending {@link BleMulticonnectProfileService} is created when the activity is created, and the activity binds to it. + * The service returns a binder that may be used to connect, disconnect or manage devices, and notifies the + * activity using Local Broadcasts ({@link LocalBroadcastManager}). See {@link BleMulticonnectProfileService} for messages. If the device is not in range it will listen for + * it and connect when it become visible. The service exists until all managed devices have been disconnected and unmanaged and the last activity unbinds from it. + *

+ *

+ * When user closes the activity (e.g. by pressing Back button) while being connected, the Service remains working. It's remains connected to the devices or still + * listens for updates from them. When entering back to the activity, activity will to bind to the service and refresh UI. + *

+ */ +public abstract class BleMulticonnectProfileServiceReadyActivity extends AppCompatActivity implements + ScannerFragment.OnDeviceSelectedListener, BleManagerCallbacks { + private static final String TAG = "BleMulticonnectProfileServiceReadyActivity"; + + protected static final int REQUEST_ENABLE_BT = 2; + + private E mService; + private List mManagedDevices; + + private final BroadcastReceiver mCommonBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BleMulticonnectProfileService.EXTRA_DEVICE); + final String action = intent.getAction(); + switch (action) { + case BleMulticonnectProfileService.BROADCAST_CONNECTION_STATE: { + final int state = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_CONNECTION_STATE, BleMulticonnectProfileService.STATE_DISCONNECTED); + + switch (state) { + case BleMulticonnectProfileService.STATE_CONNECTED: { + onDeviceConnected(bluetoothDevice); + break; + } + case BleMulticonnectProfileService.STATE_DISCONNECTED: { + onDeviceDisconnected(bluetoothDevice); + break; + } + case BleMulticonnectProfileService.STATE_LINK_LOSS: { + onLinklossOccur(bluetoothDevice); + break; + } + case BleMulticonnectProfileService.STATE_CONNECTING: { + onDeviceConnecting(bluetoothDevice); + break; + } + case BleMulticonnectProfileService.STATE_DISCONNECTING: { + onDeviceDisconnecting(bluetoothDevice); + break; + } + default: + // there should be no other actions + break; + } + break; + } + case BleMulticonnectProfileService.BROADCAST_SERVICES_DISCOVERED: { + final boolean primaryService = intent.getBooleanExtra(BleMulticonnectProfileService.EXTRA_SERVICE_PRIMARY, false); + final boolean secondaryService = intent.getBooleanExtra(BleMulticonnectProfileService.EXTRA_SERVICE_SECONDARY, false); + + if (primaryService) { + onServicesDiscovered(bluetoothDevice, secondaryService); + } else { + onDeviceNotSupported(bluetoothDevice); + } + break; + } + case BleMulticonnectProfileService.BROADCAST_DEVICE_READY: { + onDeviceReady(bluetoothDevice); + break; + } + case BleMulticonnectProfileService.BROADCAST_BOND_STATE: { + final int state = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); + switch (state) { + case BluetoothDevice.BOND_BONDING: + onBondingRequired(bluetoothDevice); + break; + case BluetoothDevice.BOND_BONDED: + onBonded(bluetoothDevice); + break; + } + break; + } + case BleMulticonnectProfileService.BROADCAST_BATTERY_LEVEL: { + final int value = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_BATTERY_LEVEL, -1); + if (value > 0) + onBatteryValueReceived(bluetoothDevice, value); + break; + } + case BleMulticonnectProfileService.BROADCAST_ERROR: { + final String message = intent.getStringExtra(BleMulticonnectProfileService.EXTRA_ERROR_MESSAGE); + final int errorCode = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_ERROR_CODE, 0); + onError(bluetoothDevice, message, errorCode); + break; + } + } + } + }; + + private ServiceConnection mServiceConnection = new ServiceConnection() { + @SuppressWarnings("unchecked") + @Override + public void onServiceConnected(final ComponentName name, final IBinder service) { + final E bleService = mService = (E) service; + bleService.log(LogContract.Log.Level.DEBUG, "Activity bound to the service"); + mManagedDevices.addAll(bleService.getManagedDevices()); + onServiceBinded(bleService); + + // and notify user if device is connected + for (final BluetoothDevice device : mManagedDevices) { + if (bleService.isConnected(device)) + onDeviceConnected(device); + } + } + + @Override + public void onServiceDisconnected(final ComponentName name) { + mService = null; + onServiceUnbinded(); + } + }; + + @Override + protected final void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mManagedDevices = new ArrayList<>(); + + ensureBLESupported(); + if (!isBLEEnabled()) { + showBLEDialog(); + } + + // In onInitialize method a final class may register local broadcast receivers that will listen for events from the service + onInitialize(savedInstanceState); + // The onCreateView class should... create the view + onCreateView(savedInstanceState); + + final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_actionbar); + setSupportActionBar(toolbar); + + // Common nRF Toolbox view references are obtained here + setUpView(); + // View is ready to be used + onViewCreated(savedInstanceState); + + LocalBroadcastManager.getInstance(this).registerReceiver(mCommonBroadcastReceiver, makeIntentFilter()); + } + + @Override + protected void onResume() { + super.onResume(); + + /* + * In comparison to BleProfileServiceReadyActivity this activity always starts the service when started. + * Connecting to a device is done by calling mService.connect(BluetoothDevice) method, not startService(...) like there. + * The service will stop itself when all devices it manages were disconnected and unmanaged and the last activity unbinds from it. + */ + final Intent service = new Intent(this, getServiceClass()); + startService(service); + bindService(service, mServiceConnection, 0); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mService != null) { + // We don't want to perform some operations (e.g. disable Battery Level notifications) in the service if we are just rotating the screen. + // However, when the activity will disappear, we may want to disable some device features to reduce the battery consumption. + mService.setActivityIsChangingConfiguration(isChangingConfigurations()); + // Log it here as there is no callback when the service gets unbound + // and the mService will not be available later (the activity doesn't keep log sessions) + mService.log(LogContract.Log.Level.DEBUG, "Activity unbound from the service"); + } + + unbindService(mServiceConnection); + mService = null; + + onServiceUnbinded(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + LocalBroadcastManager.getInstance(this).unregisterReceiver(mCommonBroadcastReceiver); + } + + private static IntentFilter makeIntentFilter() { + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_CONNECTION_STATE); + intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_SERVICES_DISCOVERED); + intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_DEVICE_READY); + intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_BOND_STATE); + intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_BATTERY_LEVEL); + intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_ERROR); + return intentFilter; + } + + /** + * Called when activity binds to the service. The parameter is the object returned in {@link Service#onBind(Intent)} method in your service. + * It is safe to obtain managed devices now. + */ + protected abstract void onServiceBinded(E binder); + + /** + * Called when activity unbinds from the service. You may no longer use this binder methods. + */ + protected abstract void onServiceUnbinded(); + + /** + * Returns the service class for sensor communication. The service class must derive from {@link BleMulticonnectProfileService} in order to operate with this class. + * + * @return the service class + */ + protected abstract Class getServiceClass(); + + /** + * Returns the service interface that may be used to communicate with the sensor. This will return null if the device is disconnected from the + * sensor. + * + * @return the service binder or null + */ + protected E getService() { + return mService; + } + + /** + * You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created. + */ + protected void onInitialize(final Bundle savedInstanceState) { + // empty default implementation + } + + /** + * Called from {@link #onCreate(Bundle)}. This method should build the activity UI, f.e. using {@link #setContentView(int)}. Use to obtain references to + * views. Connect/Disconnect button, the device name view and battery level view are manager automatically. + * + * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. Note: Otherwise it is null. + */ + protected abstract void onCreateView(final Bundle savedInstanceState); + + /** + * Called after the view has been created. + * + * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. Note: Otherwise it is null. + */ + protected void onViewCreated(final Bundle savedInstanceState) { + // empty default implementation + } + + /** + * Called after the view and the toolbar has been created. + */ + protected final void setUpView() { + // set GUI + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.help, menu); + return true; + } + + /** + * Use this method to handle menu actions other than home and about. + * + * @param itemId the menu item id + * @return true if action has been handled + */ + protected boolean onOptionsItemSelected(final int itemId) { + // Overwrite when using menu other than R.menu.help + return false; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + final int id = item.getItemId(); + switch (id) { + case android.R.id.home: + onBackPressed(); + break; + case R.id.action_about: + final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId()); + fragment.show(getSupportFragmentManager(), "help_fragment"); + break; + default: + return onOptionsItemSelected(id); + } + return true; + } + + /** + * Called when user press ADD DEVICE button. See layout files -> onClick attribute. + */ + public void onAddDeviceClicked(final View view) { + if (isBLEEnabled()) { + showDeviceScanningDialog(getFilterUUID()); + } else { + showBLEDialog(); + } + } + + /** + * Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used. + * + * @return the title resource id + */ + protected int getLoggerProfileTitle() { + return 0; + } + + /** + * This method may return the local log content provider authority if local log sessions are supported. + * + * @return local log session content provider URI + */ + protected Uri getLocalAuthorityLogger() { + return null; + } + + @Override + public void onDeviceSelected(final BluetoothDevice device, final String name) { + final int titleId = getLoggerProfileTitle(); + ILogSession logSession = null; + if (titleId > 0) { + logSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name); + // If nRF Logger is not installed we may want to use local logger + if (logSession == null && getLocalAuthorityLogger() != null) { + logSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name); + } + } + + mService.connect(device, logSession); + } + + @Override + public void onDialogCanceled() { + // do nothing + } + + @Override + public void onDeviceConnecting(final BluetoothDevice device) { + // empty default implementation + } + + @Override + public void onDeviceDisconnecting(final BluetoothDevice device) { + // empty default implementation + } + + @Override + public void onLinklossOccur(final BluetoothDevice device) { + // empty default implementation + } + + @Override + public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) { + // empty default implementation + } + + @Override + public void onDeviceReady(final BluetoothDevice device) { + // empty default implementation + } + + @Override + public void onBondingRequired(final BluetoothDevice device) { + // empty default implementation + } + + @Override + public void onBonded(final BluetoothDevice device) { + // empty default implementation + } + + @Override + public void onDeviceNotSupported(final BluetoothDevice device) { + showToast(R.string.not_supported); + } + + @Override + public final boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device) { + // This method will never be called. + // Please see BleMulticonnectProfileService#shouldEnableBatteryLevelNotifications(BluetoothDevice) instead. + throw new UnsupportedOperationException("This method should not be called"); + } + + @Override + public void onBatteryValueReceived(final BluetoothDevice device, final int value) { + // empty default implementation + } + + @Override + public void onError(final BluetoothDevice device, final String message, final int errorCode) { + DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode); + showToast(message + " (" + errorCode + ")"); + } + + /** + * 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) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(BleMulticonnectProfileServiceReadyActivity.this, message, Toast.LENGTH_LONG).show(); + } + }); + } + + /** + * 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) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(BleMulticonnectProfileServiceReadyActivity.this, messageResId, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * Returns the string resource id that will be shown in About box + * + * @return the about resource id + */ + protected abstract int getAboutTextId(); + + /** + * The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also: + * {@link #isChangingConfigurations()}. + * + * @return the required UUID or null + */ + protected abstract UUID getFilterUUID(); + + /** + * Returns unmodifiable list of managed devices. Managed device is a device the was selected on ScannerFragment until it's removed from the managed list. + * It does not have to be connected at that moment. + * @return unmodifiable list of managed devices + */ + protected List getManagedDevices() { + return Collections.unmodifiableList(mManagedDevices); + } + + /** + * Returns true if the device is connected. Services may not have been discovered yet. + * @param device the device to check if it's connected + */ + protected boolean isDeviceConnected(final BluetoothDevice device) { + return mService != null && mService.isConnected(device); + } + + /** + * Shows the scanner fragment. + * + * @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their + * services + * @see #getFilterUUID() + */ + private void showDeviceScanningDialog(final UUID filter) { + final ScannerFragment dialog = ScannerFragment.getInstance(filter); + dialog.show(getSupportFragmentManager(), "scan_fragment"); + } + + private void ensureBLESupported() { + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { + Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show(); + finish(); + } + } + + protected boolean isBLEEnabled() { + final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); + final BluetoothAdapter adapter = bluetoothManager.getAdapter(); + return adapter != null && adapter.isEnabled(); + } + + protected void showBLEDialog() { + final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableIntent, REQUEST_ENABLE_BT); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/IDeviceLogger.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/IDeviceLogger.java new file mode 100644 index 00000000..233e84f3 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/IDeviceLogger.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.profile.multiconnect; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.StringRes; + +public interface IDeviceLogger { + /** + * Logs the given message with given log level into the device's log session. + * @param device the target device + * @param level the log level + * @param message the message to be logged + */ + void log(final BluetoothDevice device, final int level, final String message); + + /** + * Logs the given message with given log level into the device's log session. + * @param device the target device + * @param level the log level + * @param messageRes string resource id + * @param params additional (optional) parameters used to fill the message + */ + void log(final BluetoothDevice device, final int level, @StringRes final int messageRes, final Object... params); +}