Support for mutliple profile with connections added

This commit is contained in:
Aleksander Nowakowski
2016-10-17 18:10:09 +02:00
parent f2fe310c67
commit 78728296dd
3 changed files with 1211 additions and 0 deletions

View File

@@ -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<BluetoothDevice, BleManager<BleManagerCallbacks>> mBleManagers;
private List<BluetoothDevice> 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<BluetoothDevice> 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<BleManagerCallbacks> 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<BleManagerCallbacks> manager = mBleManagers.get(device);
if (manager != null && manager.isConnected()) {
manager.disconnect();
}
mManagedDevices.remove(device);
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
* @param device the target device
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
public final boolean isConnected(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> 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<BleManagerCallbacks> 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<BleManagerCallbacks> manager = mBleManagers.get(device);
return manager.getBatteryValue();
}
/**
* Sets whether the bound activity if changing configuration or not.
* If <code>false</code>, 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<BleManagerCallbacks> 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<BleManagerCallbacks> 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<BleManagerCallbacks> manager : mBleManagers.values())
manager.log(level, message);
}
@Override
public void log(final int level, @StringRes final int messageRes, final Object... params) {
for (final BleManager<BleManagerCallbacks> 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<BleManagerCallbacks> 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<BleManagerCallbacks> 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<BleManagerCallbacks> 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<BleManagerCallbacks> 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 <code>super.onBluetoothEnabled()</code> 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<BleManagerCallbacks> 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<BleManagerCallbacks> 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<? extends BleManagerCallbacks> 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<BluetoothDevice> 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<BluetoothDevice> getConnectedDevices() {
final List<BluetoothDevice> list = new ArrayList<>();
for (BluetoothDevice device : mManagedDevices) {
if (mBleManagers.get(device).isConnected())
list.add(device);
}
return Collections.unmodifiableList(list);
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
* @param device the target device
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
protected boolean isConnected(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
return manager != null && manager.isConnected();
}
}

View File

@@ -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;
/**
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*/
public abstract class BleMulticonnectProfileServiceReadyActivity<E extends BleMulticonnectProfileService.LocalBinder> 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<BluetoothDevice> 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<? extends BleMulticonnectProfileService> getServiceClass();
/**
* Returns the service interface that may be used to communicate with the sensor. This will return <code>null</code> if the device is disconnected from the
* sensor.
*
* @return the service binder or <code>null</code>
*/
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: <b>Otherwise it is null</b>.
*/
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: <b>Otherwise it is null</b>.
*/
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 <code>true</code> 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 <code>null</code>
*/
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<BluetoothDevice> getManagedDevices() {
return Collections.unmodifiableList(mManagedDevices);
}
/**
* Returns <code>true</code> 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);
}
}

View File

@@ -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);
}