Implementing request queue in BleManager

Both BleManagers (app and wear) modified,
All profiles adjusted to match new API,
Supporting Bluetooth OFF/ON event in BleProfileService
This commit is contained in:
Aleksander Nowakowski
2016-10-17 18:00:44 +02:00
parent 0d898e0557
commit 0b2e3aba17
31 changed files with 1885 additions and 556 deletions

View File

@@ -27,14 +27,14 @@ import android.bluetooth.BluetoothGattService;
import android.content.Context;
import java.util.Calendar;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
import no.nordicsemi.android.nrftoolbox.parser.BloodPressureMeasurementParser;
import no.nordicsemi.android.nrftoolbox.parser.IntermediateCuffPressureParser;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
public class BPMManager extends BleManager<BPMManagerCallbacks> {
/** Blood Pressure service UUID */
@@ -73,7 +73,7 @@ public class BPMManager extends BleManager<BPMManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
if (mICPCharacteristic != null)
requests.add(Request.newEnableNotificationsRequest(mICPCharacteristic));

View File

@@ -28,8 +28,8 @@ import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.util.SparseArray;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
@@ -128,7 +128,7 @@ public class CGMSManager extends BleManager<CGMSManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.add(Request.newEnableNotificationsRequest(mCGMMeasurementCharacteristic));
if (mCGMOpsControlPointCharacteristic != null) {

View File

@@ -161,12 +161,6 @@ public class CGMService extends BleProfileService implements CGMSManagerCallback
createNotification(R.string.csc_notification_connected_message, 0);
}
@Override
protected void onServiceStarted() {
// logger is now available. Assign it to the manager
mManager.setLogger(getLogSession());
}
/**
* Creates the notification
*

View File

@@ -27,13 +27,13 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
import no.nordicsemi.android.nrftoolbox.parser.CSCMeasurementParser;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
public class CSCManager extends BleManager<CSCManagerCallbacks> {
/** Cycling Speed and Cadence service UUID */
@@ -61,7 +61,7 @@ public class CSCManager extends BleManager<CSCManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.add(Request.newEnableNotificationsRequest(mCSCMeasurementCharacteristic));
return requests;

View File

@@ -120,12 +120,6 @@ public class CSCService extends BleProfileService implements CSCManagerCallbacks
createNotification(R.string.csc_notification_connected_message, 0);
}
@Override
protected void onServiceStarted() {
// logger is now available. Assign it to the manager
mManager.setLogger(getLogSession());
}
@Override
public void onWheelMeasurementReceived(final BluetoothDevice device, final int wheelRevolutions, final int lastWheelEventTime) {
Logger.a(getLogSession(), "Wheel rev: " + wheelRevolutions + "\nLast wheel event time: " + lastWheelEventTime + " ms");

View File

@@ -29,15 +29,15 @@ import android.os.Handler;
import android.util.SparseArray;
import java.util.Calendar;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
import no.nordicsemi.android.nrftoolbox.parser.GlucoseMeasurementContextParser;
import no.nordicsemi.android.nrftoolbox.parser.GlucoseMeasurementParser;
import no.nordicsemi.android.nrftoolbox.parser.RecordAccessControlPointParser;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
@SuppressWarnings("unused")
@@ -127,7 +127,7 @@ public class GlucoseManager extends BleManager<GlucoseManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.add(Request.newEnableNotificationsRequest(mGlucoseMeasurementCharacteristic));
if (mGlucoseMeasurementContextCharacteristic != null)

View File

@@ -26,15 +26,15 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
import no.nordicsemi.android.nrftoolbox.parser.BodySensorLocationParser;
import no.nordicsemi.android.nrftoolbox.parser.HeartRateMeasurementParser;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
/**
* HRSManager class performs BluetoothGatt operations for connection, service discovery, enabling notification and reading characteristics. All operations required to connect to device with BLE HR
@@ -74,7 +74,7 @@ public class HRSManager extends BleManager<HRSManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
if (mHRLocationCharacteristic != null)
requests.add(Request.newReadRequest(mHRLocationCharacteristic));

View File

@@ -26,8 +26,8 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
@@ -71,7 +71,7 @@ public class HTSManager extends BleManager<HTSManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.add(Request.newEnableIndicationsRequest(mHTCharacteristic));
return requests;

View File

@@ -100,12 +100,6 @@ public class HTSService extends BleProfileService implements HTSManagerCallbacks
createNotification(R.string.hts_notification_connected_message, 0);
}
@Override
protected void onServiceStarted() {
// logger is now available. Assign it to the manager
mManager.setLogger(getLogSession());
}
@Override
public void onHTValueReceived(final BluetoothDevice device, final double value) {
final Intent broadcast = new Intent(BROADCAST_HTS_MEASUREMENT);

View File

@@ -86,6 +86,18 @@ public interface BleManagerCallbacks {
*/
void onDeviceReady(final BluetoothDevice device);
/**
* This method should return true if Battery Level notifications should be enabled on the target device.
* If there is no Battery Service, or the Battery Level characteristic does not have NOTIFY property,
* this method will not be called for this device.
* <p>This method may return true only if an activity is bound to the service (to display the information
* to the user), always (e.g. if critical battery level is reported using notifications) or never, if
* such information is not important or the manager wants to control Battery Level notifications on its own.</p>
* @param device target device
* @return true to enabled battery level notifications after connecting to the device, false otherwise
*/
boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device);
/**
* Called when battery value has been received from the device.
*

View File

@@ -52,9 +52,9 @@ import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
public abstract class BleProfileActivity extends AppCompatActivity implements BleManagerCallbacks, ScannerFragment.OnDeviceSelectedListener {
private static final String TAG = "BaseProfileActivity";
private static final String CONNECTION_STATUS = "connection_status";
private static final String DEVICE_NAME = "device_name";
private static final int REQUEST_ENABLE_BT = 2;
private static final String SIS_CONNECTION_STATUS = "connection_status";
private static final String SIS_DEVICE_NAME = "device_name";
protected static final int REQUEST_ENABLE_BT = 2;
private BleManager<? extends BleManagerCallbacks> mBleManager;
@@ -82,12 +82,18 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
* broadcast listeners, local broadcast listeners (see support.v4 library), or messages. See the Proximity profile for Service approach.
*/
mBleManager = initializeManager();
// 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);
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);
}
@@ -101,18 +107,24 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
/**
* 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>.
*
* @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
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. Note: <b>Otherwise it is null</b>.
*/
protected final void onViewCreated(final Bundle savedInstanceState) {
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);
mConnectButton = (Button) findViewById(R.id.action_connect);
@@ -129,15 +141,15 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
@Override
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(CONNECTION_STATUS, mDeviceConnected);
outState.putString(DEVICE_NAME, mDeviceName);
outState.putBoolean(SIS_CONNECTION_STATUS, mDeviceConnected);
outState.putString(SIS_DEVICE_NAME, mDeviceName);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mDeviceConnected = savedInstanceState.getBoolean(CONNECTION_STATUS);
mDeviceName = savedInstanceState.getString(DEVICE_NAME);
mDeviceConnected = savedInstanceState.getBoolean(SIS_CONNECTION_STATUS);
mDeviceName = savedInstanceState.getString(SIS_DEVICE_NAME);
if (mDeviceConnected) {
mConnectButton.setText(R.string.action_disconnect);
@@ -154,9 +166,8 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
/**
* Use this method to handle menu actions other than home and about.
*
* @param itemId
* the menu item id
*
* @param itemId the menu item id
* @return <code>true</code> if action has been handled
*/
protected boolean onOptionsItemSelected(final int itemId) {
@@ -168,15 +179,15 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
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);
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;
}
@@ -229,7 +240,6 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
mBleManager.setLogger(mLogSession);
mDeviceNameView.setText(name != null ? name : getString(R.string.not_available));
mConnectButton.setText(R.string.action_connecting);
mConnectButton.setEnabled(false);
mBleManager.connect(device);
}
@@ -250,7 +260,6 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
@Override
public void run() {
mConnectButton.setText(R.string.action_disconnect);
mConnectButton.setEnabled(true);
}
});
}
@@ -270,7 +279,6 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
mConnectButton.setText(R.string.action_connect);
mDeviceNameView.setText(getDefaultDeviceName());
mBatteryLevelView.setText(R.string.not_available);
mConnectButton.setEnabled(true);
}
});
}
@@ -281,21 +289,20 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
runOnUiThread(new Runnable() {
@Override
public void run() {
mConnectButton.setText(R.string.action_connect);
mDeviceNameView.setText(getDefaultDeviceName());
mBatteryLevelView.setText(R.string.not_available);
if (mBatteryLevelView != null)
mBatteryLevelView.setText(R.string.not_available);
}
});
}
@Override
public void onBatteryValueReceived(final BluetoothDevice device, final int value) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mBatteryLevelView.setText(getString(R.string.battery, value));
}
});
public void onServicesDiscovered(final BluetoothDevice device, boolean optionalServicesFound) {
// this may notify user or show some views
}
@Override
public void onDeviceReady(final BluetoothDevice device) {
// empty default implementation
}
@Override
@@ -308,6 +315,23 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
showToast(R.string.bonded);
}
@Override
public boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device) {
// Yes, we want battery level updates
return true;
}
@Override
public void onBatteryValueReceived(final BluetoothDevice device, final int value) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mBatteryLevelView != null)
mBatteryLevelView.setText(getString(R.string.battery, value));
}
});
}
@Override
public void onError(final BluetoothDevice device, final String message, final int errorCode) {
DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode);
@@ -321,9 +345,8 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
/**
* 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
*
* @param message a message to be shown
*/
protected void showToast(final String message) {
runOnUiThread(new Runnable() {
@@ -336,9 +359,8 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
/**
* 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
*
* @param messageResId an resource id of the message to be shown
*/
protected void showToast(final int messageResId) {
runOnUiThread(new Runnable() {
@@ -365,7 +387,7 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
/**
* Initializes the Bluetooth Low Energy manager. A manager is used to communicate with profile's services.
*
*
* @return the manager that was created
*/
protected abstract BleManager<? extends BleManagerCallbacks> initializeManager();
@@ -378,14 +400,14 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
/**
* Returns the default device name resource id. The real device name is obtained when connecting to the device. This one is used when device has
* disconnected.
*
*
* @return the default device name resource id
*/
protected abstract int getDefaultDeviceName();
/**
* Returns the string resource id that will be shown in About box
*
*
* @return the about resource id
*/
protected abstract int getAboutTextId();
@@ -393,17 +415,16 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
/**
* 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();
/**
* 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
*
* @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) {
@@ -416,6 +437,15 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl
});
}
/**
* Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT.
*
* @return the logger session or <code>null</code>
*/
protected ILogSession getLogSession() {
return mLogSession;
}
private void ensureBLESupported() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show();

View File

@@ -29,6 +29,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
@@ -81,12 +82,18 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
* broadcast listeners, local broadcast listeners (see support.v4 library), or messages. See the Proximity profile for Service approach.
*/
mBleManager = initializeManager();
// 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);
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);
}
@@ -110,7 +117,14 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. Note: <b>Otherwise it is null</b>.
*/
protected final void onViewCreated(final Bundle savedInstanceState) {
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);
mConnectButton = (Button) findViewById(R.id.action_connect);
@@ -132,7 +146,7 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
}
@Override
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mDeviceConnected = savedInstanceState.getBoolean(SIS_CONNECTION_STATUS);
mDeviceName = savedInstanceState.getString(SIS_DEVICE_NAME);
@@ -226,7 +240,6 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
mBleManager.setLogger(mLogSession);
mDeviceNameView.setText(name != null ? name : getString(R.string.not_available));
mConnectButton.setText(R.string.action_connecting);
mConnectButton.setEnabled(false);
mBleManager.connect(device);
}
@@ -247,7 +260,6 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
@Override
public void run() {
mConnectButton.setText(R.string.action_disconnect);
mConnectButton.setEnabled(true);
}
});
}
@@ -277,9 +289,8 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
runOnUiThread(new Runnable() {
@Override
public void run() {
mConnectButton.setText(R.string.action_connect);
mDeviceNameView.setText(getDefaultDeviceName());
mBatteryLevelView.setText(R.string.not_available);
if (mBatteryLevelView != null)
mBatteryLevelView.setText(R.string.not_available);
}
});
}
@@ -289,23 +300,11 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
// this may notify user or show some views
}
/**
* Called when the initialization process in completed.
*/
@Override
public void onDeviceReady(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBatteryValueReceived(final BluetoothDevice device, final int value) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mBatteryLevelView.setText(getString(R.string.battery, value));
}
});
}
@Override
public void onBondingRequired(final BluetoothDevice device) {
showToast(R.string.bonding);
@@ -316,6 +315,23 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
showToast(R.string.bonded);
}
@Override
public boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device) {
// Yes, we want battery level updates
return true;
}
@Override
public void onBatteryValueReceived(final BluetoothDevice device, final int value) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mBatteryLevelView != null)
mBatteryLevelView.setText(getString(R.string.battery, value));
}
});
}
@Override
public void onError(final BluetoothDevice device, final String message, final int errorCode) {
DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode);
@@ -421,6 +437,15 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct
});
}
/**
* Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT.
*
* @return the logger session or <code>null</code>
*/
protected ILogSession getLogSession() {
return mLogSession;
}
private void ensureBLESupported() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show();

View File

@@ -24,16 +24,22 @@ package no.nordicsemi.android.nrftoolbox.profile;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.support.annotation.StringRes;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.log.Logger;
public abstract class BleProfileService extends Service implements BleManagerCallbacks {
@@ -71,18 +77,54 @@ public abstract class BleProfileService extends Service implements BleManagerCal
private Handler mHandler;
protected boolean mBinded;
private boolean mActivityFinished;
private boolean mConnected;
private boolean mActivityIsChangingConfiguration;
private BluetoothDevice mBluetoothDevice;
private String mDeviceName;
private ILogSession mLogSession;
public class LocalBinder extends Binder {
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 {
/**
* Disconnects from the sensor.
*/
public final void disconnect() {
if (!mConnected) {
final int state = mBleManager.getConnectionState();
if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) {
mBleManager.close();
onDeviceDisconnected(mBluetoothDevice);
return;
@@ -92,16 +134,17 @@ public abstract class BleProfileService extends Service implements BleManagerCal
}
/**
* Sets whether the binded activity if finishing or not. If <code>true</code>, we will turn off battery level notifications in onUnbind(..) method below.
* @param finishing true if the binded activity is finishing
* 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 void setActivityIsFinishing(final boolean finishing) {
mActivityFinished = finishing;
public void setActivityIsChangingConfiguration(final boolean changing) {
mActivityIsChangingConfiguration = changing;
}
/**
* Returns the device address
*
*
* @return device address
*/
public String getDeviceAddress() {
@@ -110,7 +153,7 @@ public abstract class BleProfileService extends Service implements BleManagerCal
/**
* Returns the device name
*
*
* @return the device name
*/
public String getDeviceName() {
@@ -128,26 +171,46 @@ public abstract class BleProfileService extends Service implements BleManagerCal
/**
* Returns <code>true</code> if the device is connected to the sensor.
*
*
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
public boolean isConnected() {
return mConnected;
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 <code>null</code> if the nRF Logger app was
* not installed.
* 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 <code>null</code> if the nRF Logger app was not installed.
*
* @return the log session
*/
public ILogSession getLogSession() {
return mLogSession;
}
@Override
public void log(final int level, 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 the binder implementation. This must return class implementing the additional manager interface that may be used in the binded activity.
* 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
*/
@@ -166,11 +229,10 @@ public abstract class BleProfileService extends Service implements BleManagerCal
public final void onRebind(final Intent intent) {
mBinded = true;
if (mActivityFinished)
if (!mActivityIsChangingConfiguration)
onRebind();
if (mActivityFinished && mConnected) {
mActivityFinished = false;
if (!mActivityIsChangingConfiguration && mBleManager.isConnected()) {
// This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property).
// If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications.
mBleManager.readBatteryLevel();
@@ -178,33 +240,35 @@ public abstract class BleProfileService extends Service implements BleManagerCal
}
/**
* Called when the activity has rebinded to the service after being recreated. This method is not called when the activity was killed and recreated just to change the phone orientation.
* 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
// empty default implementation
}
@Override
public final boolean onUnbind(final Intent intent) {
mBinded = false;
if (mActivityFinished)
if (!mActivityIsChangingConfiguration)
onUnbind();
// When we are connected, but the application is not open, we are not really interested in battery level notifications. But we will still be receiving other values, if enabled.
if (mActivityFinished && mConnected)
if (!mActivityIsChangingConfiguration && mBleManager.isConnected())
mBleManager.setBatteryNotifications(false);
// we must allow to rebind to the same service
// 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 just to change the phone orientation.
* This method is not called when the activity is killed to be recreated when the phone orientation changed.
*/
protected void onUnbind() {
// empty
// empty default implementation
}
@SuppressWarnings("unchecked")
@@ -214,11 +278,34 @@ public abstract class BleProfileService extends Service implements BleManagerCal
mHandler = new Handler();
// initialize the manager
// 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 BleManager initializeManager();
@@ -237,22 +324,35 @@ public abstract class BleProfileService extends Service implements BleManagerCal
final BluetoothAdapter adapter = bluetoothManager.getAdapter();
final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
mBluetoothDevice = adapter.getRemoteDevice(deviceAddress);
onServiceStarted();
mBleManager.setLogger(mLogSession);
onServiceStarted();
mBleManager.connect(mBluetoothDevice);
return START_REDELIVER_INTENT;
}
/**
* Called when the service has been started. The device name and address are set. It nRF Logger is installed than logger was also initialized.
* 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();
@@ -260,10 +360,31 @@ public abstract class BleProfileService extends Service implements BleManagerCal
mBleManager = null;
mBluetoothDevice = null;
mDeviceName = null;
mConnected = false;
mLogSession = 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 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);
@@ -274,8 +395,6 @@ public abstract class BleProfileService extends Service implements BleManagerCal
@Override
public void onDeviceConnected(final BluetoothDevice device) {
mConnected = true;
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
@@ -303,8 +422,6 @@ public abstract class BleProfileService extends Service implements BleManagerCal
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
mConnected = false;
// 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, mBluetoothDevice);
@@ -323,8 +440,6 @@ public abstract class BleProfileService extends Service implements BleManagerCal
@Override
public void onLinklossOccur(final BluetoothDevice device) {
mConnected = false;
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS);
@@ -394,6 +509,8 @@ public abstract class BleProfileService extends Service implements BleManagerCal
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.
mBleManager.disconnect();
stopSelf();
}
@@ -471,6 +588,6 @@ public abstract class BleProfileService extends Service implements BleManagerCal
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
protected boolean isConnected() {
return mConnected;
return mBleManager.isConnected();
}
}

View File

@@ -179,21 +179,30 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
final E bleService = mService = (E) service;
mBluetoothDevice = bleService.getBluetoothDevice();
mLogSession = mService.getLogSession();
Logger.d(mLogSession, "Activity binded to the service");
Logger.d(mLogSession, "Activity bound to the service");
onServiceBinded(bleService);
// update UI
// Update UI
mDeviceName = bleService.getDeviceName();
mDeviceNameView.setText(mDeviceName);
mConnectButton.setText(R.string.action_disconnect);
// and notify user if device is connected
if (bleService.isConnected())
// And notify user if device is connected
if (bleService.isConnected()) {
onDeviceConnected(mBluetoothDevice);
} else {
// If the device is not connected it means that either it is still connecting,
// or the link was lost and service is trying to connect to it (autoConnect=true).
onDeviceConnecting(mBluetoothDevice);
}
}
@Override
public void onServiceDisconnected(final ComponentName name) {
// Note: this method is called only when the service is killed by the system,
// not when it stops itself or is stopped by the activity.
// It will be called only when there is critically low memory, in practice never
// when the activity is in foreground.
Logger.d(mLogSession, "Activity disconnected from the service");
mDeviceNameView.setText(getDefaultDeviceName());
mConnectButton.setText(R.string.action_connect);
@@ -221,33 +230,32 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
mLogSession = Logger.openSession(getApplicationContext(), logUri);
}
/*
* In this example we use the ProximityManager in the service. This class communicates with the service using local broadcasts. Final activity may bind
* to the Server to use its interface.
*/
// 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);
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 onStart() {
super.onStart();
protected void onResume() {
super.onResume();
/*
* If the service has not been started before, the following lines will not start it. However, if it's running, the Activity will be binded to it and
* If the service has not been started before, the following lines will not start it. However, if it's running, the Activity will bind to it and
* notified via mServiceConnection.
*/
final Intent service = new Intent(this, getServiceClass());
if (bindService(service, mServiceConnection, 0)) // we pass 0 as a flag so the service will not be created if not exists
Logger.d(mLogSession, "Binding to the service..."); // (* - see the comment below)
bindService(service, mServiceConnection, 0); // we pass 0 as a flag so the service will not be created if not exists
/*
* * - When user exited the UARTActivity while being connected, the log session is kept in the service. We may not get it before binding to it so in this
@@ -256,20 +264,19 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
}
@Override
protected void onStop() {
super.onStop();
protected void onPause() {
super.onPause();
try {
// 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 is finishing, we may want to disable some device features to reduce the battery consumption.
// However, when the activity will disappear, we may want to disable some device features to reduce the battery consumption.
if (mService != null)
mService.setActivityIsFinishing(isFinishing());
mService.setActivityIsChangingConfiguration(isChangingConfigurations());
Logger.d(mLogSession, "Unbinding from the service...");
unbindService(mServiceConnection);
mService = null;
Logger.d(mLogSession, "Activity unbinded from the service");
Logger.d(mLogSession, "Activity unbound from the service");
onServiceUnbinded();
mDeviceName = null;
mBluetoothDevice = null;
@@ -460,7 +467,6 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
mDeviceName = name;
mDeviceNameView.setText(name != null ? name : getString(R.string.not_available));
mConnectButton.setText(R.string.action_connecting);
mConnectButton.setEnabled(false);
// The device may not be in the range but the service will try to connect to it if it reach it
Logger.d(mLogSession, "Creating service...");
@@ -488,7 +494,6 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
public void onDeviceConnected(final BluetoothDevice device) {
mDeviceNameView.setText(mDeviceName);
mConnectButton.setText(R.string.action_disconnect);
mConnectButton.setEnabled(true);
}
@Override
@@ -545,8 +550,10 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
}
@Override
public void onDeviceNotSupported(final BluetoothDevice device) {
showToast(R.string.not_supported);
public final boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device) {
// This method will never be called.
// Please see BleProfileService#shouldEnableBatteryLevelNotifications(BluetoothDevice) instead.
throw new UnsupportedOperationException("This method should not be called");
}
@Override
@@ -561,6 +568,11 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
showToast(message + " (" + errorCode + ")");
}
@Override
public void onDeviceNotSupported(final BluetoothDevice device) {
showToast(R.string.not_supported);
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
@@ -593,7 +605,7 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
* Returns <code>true</code> if the device is connected. Services may not have been discovered yet.
*/
protected boolean isDeviceConnected() {
return mService != null;
return mService != null && mService.isConnected();
}
/**
@@ -658,7 +670,7 @@ public abstract class BleProfileServiceReadyActivity<E extends BleProfileService
*
* @return the logger session or <code>null</code>
*/
public ILogSession getLogSession() {
protected ILogSession getLogSession() {
return mLogSession;
}

View File

@@ -0,0 +1,43 @@
/*
* 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;
import android.support.annotation.StringRes;
public interface ILogger {
/**
* Logs the given message with given log level into the all managed devices' log session.
* @param level the log level
* @param message the message to be logged
*/
void log(final int level, final String message);
/**
* Logs the given message with given log level into the all managed devices' log session.
* @param level the log level
* @param messageRes string resource id
* @param params additional (optional) parameters used to fill the message
*/
void log(final int level, @StringRes final int messageRes, final Object... params);
}

View File

@@ -35,8 +35,8 @@ import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.error.GattError;
@@ -277,7 +277,7 @@ public class ProximityManager extends BleManager<ProximityManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.add(Request.newWriteRequest(mLinklossCharacteristic, HIGH_ALERT));
return requests;

View File

@@ -163,12 +163,6 @@ public class ProximityService extends BleProfileService implements ProximityMana
createNotification(R.string.proximity_notification_linkloss_alert, 0);
}
@Override
protected void onServiceStarted() {
// logger is now available. Assign it to the manager
mProximityManager.setLogger(getLogSession());
}
@Override
public void onDeviceDisconnecting(final BluetoothDevice device) {
stopAlarm();

View File

@@ -27,13 +27,13 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
import no.nordicsemi.android.nrftoolbox.parser.RSCMeasurementParser;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
public class RSCManager extends BleManager<RSCManagerCallbacks> {
private static final byte INSTANTANEOUS_STRIDE_LENGTH_PRESENT = 0x01; // 1 bit
@@ -62,7 +62,7 @@ public class RSCManager extends BleManager<RSCManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.add(Request.newEnableNotificationsRequest(mRSCMeasurementCharacteristic));
return requests;

View File

@@ -121,12 +121,6 @@ public class RSCService extends BleProfileService implements RSCManagerCallbacks
createNotification(R.string.rsc_notification_connected_message, 0);
}
@Override
protected void onServiceStarted() {
// logger is now available. Assign it to the manager
mManager.setLogger(getLogSession());
}
private final Runnable mUpdateStridesTask = new Runnable() {
@Override
public void run() {

View File

@@ -26,13 +26,13 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
import no.nordicsemi.android.nrftoolbox.parser.TemplateParser;
import no.nordicsemi.android.nrftoolbox.profile.BleManager;
/**
* Modify to template manager to match your requirements.
@@ -63,7 +63,7 @@ public class TemplateManager extends BleManager<TemplateManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
// TODO initialize your device, enable required notifications and indications, write what needs to be written to start working
requests.add(Request.newEnableNotificationsRequest(mCharacteristic));

View File

@@ -106,12 +106,6 @@ public class TemplateService extends BleProfileService implements TemplateManage
createNotification(R.string.template_notification_connected_message, 0);
}
@Override
protected void onServiceStarted() {
// logger is now available. Assign it to the manager
mManager.setLogger(getLogSession());
}
@Override
public void onSampleValueReceived(final BluetoothDevice device, final int value) {
final Intent broadcast = new Intent(BROADCAST_TEMPLATE_MEASUREMENT);

View File

@@ -29,8 +29,8 @@ import android.content.Context;
import android.text.TextUtils;
import java.io.UnsupportedEncodingException;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import no.nordicsemi.android.log.Logger;
@@ -65,7 +65,7 @@ public class UARTManager extends BleManager<UARTManagerCallbacks> {
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
@Override
protected Queue<Request> initGatt(final BluetoothGatt gatt) {
protected Deque<Request> initGatt(final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.add(Request.newEnableNotificationsRequest(mTXCharacteristic));
return requests;
@@ -116,11 +116,8 @@ public class UARTManager extends BleManager<UARTManagerCallbacks> {
mOutgoingBuffer = null;
} else { // Otherwise...
final int length = Math.min(buffer.length - mBufferOffset, MAX_PACKET_SIZE);
final byte[] data = new byte[length]; // We send at most 20 bytes
System.arraycopy(buffer, mBufferOffset, data, 0, length);
enqueue(Request.newWriteRequest(mRXCharacteristic, buffer, mBufferOffset, length));
mBufferOffset += length;
mRXCharacteristic.setValue(data);
writeCharacteristic(mRXCharacteristic);
}
}
@@ -158,15 +155,12 @@ public class UARTManager extends BleManager<UARTManagerCallbacks> {
if (!writeRequest) { // no WRITE REQUEST property
final int length = Math.min(buffer.length, MAX_PACKET_SIZE);
final byte[] data = new byte[length]; // We send at most 20 bytes
System.arraycopy(buffer, 0, data, 0, length);
mBufferOffset += length;
mRXCharacteristic.setValue(data);
} else { // there is WRITE REQUEST property
mRXCharacteristic.setValue(buffer);
enqueue(Request.newWriteRequest(mRXCharacteristic, buffer, 0, length));
} else { // there is WRITE REQUEST property, let's try Long Write
mBufferOffset = buffer.length;
enqueue(Request.newWriteRequest(mRXCharacteristic, buffer, 0, buffer.length));
}
writeCharacteristic(mRXCharacteristic);
}
}
}

View File

@@ -83,11 +83,6 @@ public class UARTService extends BleProfileService implements UARTManagerCallbac
public void send(final String text) {
mManager.send(text);
}
@Override
public ILogSession getLogSession() {
return super.getLogSession();
}
}
@Override
@@ -138,9 +133,9 @@ public class UARTService extends BleProfileService implements UARTManagerCallbac
}
@Override
protected void onServiceStarted() {
// logger is now available. Assign it to the manager
mManager.setLogger(getLogSession());
public boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device) {
// No UI in UART profile for Battery Level information
return false;
}
@Override

View File

@@ -51,4 +51,17 @@ public class ParserUtils {
}
return "(0x) " + new String(out);
}
public static String parseDebug(final byte[] data) {
if (data == null || data.length == 0)
return "";
final char[] out = new char[data.length * 2];
for (int j = 0; j < data.length; j++) {
int v = data[j] & 0xFF;
out[j * 2] = HEX_ARRAY[v >>> 4];
out[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return "0x" + new String(out);
}
}

View File

@@ -32,8 +32,11 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Handler;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
@@ -49,7 +52,7 @@ import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
* <li>The manager tries to read the Battery Level characteristic. No matter the result of this operation (for example the Battery Level characteristic may not have the READ property)
* it tries to enable Battery Level notifications, to get battery updates from the device.</li>
* <li>Afterwards, the manager initializes the device using given queue of commands. See {@link BleProfile#initGatt(BluetoothGatt)} method for more details.</li>
* <li>When initialization complete, the {@link BleManagerCallbacks#onDeviceReady()} callback is called.</li>
* <li>When initialization complete, the {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} callback is called.</li>
* </ol>
* </p>
* <p>Events from all profiles are being logged into the nRF Logger application,
@@ -59,24 +62,35 @@ import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
public class BleManager implements BleProfileApi {
private final static String TAG = "BleManager";
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private final static UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private final static UUID BATTERY_SERVICE = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb");
private final static UUID BATTERY_LEVEL_CHARACTERISTIC = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb");
private final static UUID GENERIC_ATTRIBUTE_SERVICE = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb");
private final static UUID SERVICE_CHANGED_CHARACTERISTIC = UUID.fromString("00002A05-0000-1000-8000-00805f9b34fb");
private final static String ERROR_CONNECTION_STATE_CHANGE = "Error on connection state change";
private final static String ERROR_DISCOVERY_SERVICE = "Error on discovering services";
private final static String ERROR_AUTH_ERROR_WHILE_BONDED = "Phone has lost bonding information";
private final static String ERROR_WRITE_DESCRIPTOR = "Error on writing descriptor";
private final static String ERROR_READ_CHARACTERISTIC = "Error on reading characteristic";
private final Object mLock = new Object();
protected final BleManagerCallbacks mCallbacks;
protected BleProfile mProfile;
private final Context mContext;
protected BluetoothDevice mBluetoothDevice;
protected BleProfile mProfile;
private Handler mHandler;
private BluetoothGatt mBluetoothGatt;
private BleManagerGattCallback mGattCallback;
/**
* This flag is set to false only when the {@link #shouldAutoConnect()} method returns true and the device got disconnected without calling {@link #disconnect()} method.
* If {@link #shouldAutoConnect()} returns false (default) this is always set to true.
*/
private boolean mUserDisconnected;
/**
* Flag set to true when the device is connected.
*/
private boolean mConnected;
private int mConnectionState = BluetoothGatt.STATE_DISCONNECTED;
/** Last received battery value or -1 if value wasn't received. */
private int mBatteryValue = -1;
private BroadcastReceiver mBondingBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -93,10 +107,10 @@ public class BleManager implements BleProfileApi {
switch (bondState) {
case BluetoothDevice.BOND_BONDING:
mCallbacks.onBondingRequired();
mCallbacks.onBondingRequired(device);
break;
case BluetoothDevice.BOND_BONDED:
mCallbacks.onBonded();
mCallbacks.onBonded(device);
// Start initializing again.
// In fact, bonding forces additional, internal service discovery (at least on Nexus devices), so this method may safely be used to start this process again.
@@ -110,7 +124,6 @@ public class BleManager implements BleProfileApi {
mCallbacks = callbacks;
mContext = context;
mHandler = new Handler();
mUserDisconnected = false;
// Register bonding broadcast receiver
context.registerReceiver(mBondingBroadcastReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
@@ -152,31 +165,64 @@ public class BleManager implements BleProfileApi {
if (mConnected)
return;
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mBluetoothGatt = null;
synchronized (mLock) {
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mBluetoothGatt = null;
}
}
final boolean autoConnect = shouldAutoConnect();
mUserDisconnected = !autoConnect; // We will receive Linkloss events only when the device is connected with autoConnect=true
mBluetoothGatt = device.connectGatt(mContext, autoConnect, mGattCallback);
mBluetoothDevice = device;
mConnectionState = BluetoothGatt.STATE_CONNECTING;
mBluetoothGatt = device.connectGatt(mContext, autoConnect, mGattCallback = new BleManagerGattCallback());
}
/**
* Disconnects from the device. Does nothing if not connected.
*
* @return true if device is to be disconnected. False if it was already disconnected.
*/
public boolean disconnect() {
mUserDisconnected = true;
if (mConnected && mBluetoothGatt != null) {
mCallbacks.onDeviceDisconnecting();
mConnectionState = BluetoothGatt.STATE_DISCONNECTING;
mCallbacks.onDeviceDisconnecting(mBluetoothGatt.getDevice());
mBluetoothGatt.disconnect();
return true;
}
return false;
}
/**
* This method returns true if the device is connected. Services could have not been discovered yet.
*/
public boolean isConnected() {
return mConnected;
}
/**
* Method returns the connection state:
* {@link BluetoothGatt#STATE_CONNECTING STATE_CONNECTING},
* {@link BluetoothGatt#STATE_CONNECTED STATE_CONNECTED},
* {@link BluetoothGatt#STATE_DISCONNECTING STATE_DISCONNECTING},
* {@link BluetoothGatt#STATE_DISCONNECTED STATE_DISCONNECTED}
* @return the connection state
*/
public int getConnectionState() {
return mConnectionState;
}
/**
* Returns the last received value of Battery Level characteristic, or -1 if such does not exist, hasn't been read or notification wasn't received yet.
* @return the last battery level value in percent
*/
public int getBatteryValue() {
return mBatteryValue;
}
/**
* Closes and releases resources. May be also used to unregister broadcast listeners.
*/
@@ -186,22 +232,27 @@ public class BleManager implements BleProfileApi {
} catch (Exception e) {
// the receiver must have been not registered or unregistered before
}
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mBluetoothGatt = null;
synchronized (mLock) {
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mBluetoothGatt = null;
}
mConnected = false;
mConnectionState = BluetoothGatt.STATE_DISCONNECTED;
mGattCallback = null;
mBluetoothDevice = null;
}
mUserDisconnected = false;
}
/**
* When the device is bonded and has the Generic Attribute service and the Service Changed characteristic this method enables indications on this characteristic.
* In case one of the requirements is not fulfilled this method returns <code>false</code>.
*
* @param gatt the gatt device with services discovered
* @return <code>true</code> when the request has been sent, <code>false</code> when the device is not bonded, does not have the Generic Attribute service, the GA service does not have
* the Service Changed characteristic or this characteristic does not have the CCCD.
*/
private boolean ensureServiceChangedEnabled(final BluetoothGatt gatt) {
private boolean ensureServiceChangedEnabled() {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null)
return false;
@@ -223,6 +274,10 @@ public class BleManager implements BleProfileApi {
@Override
public final boolean enableNotifications(final BluetoothGattCharacteristic characteristic) {
return enqueue(Request.newEnableNotificationsRequest(characteristic));
}
private boolean internalEnableNotifications(final BluetoothGattCharacteristic characteristic) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null || characteristic == null)
return false;
@@ -243,6 +298,10 @@ public class BleManager implements BleProfileApi {
@Override
public final boolean enableIndications(final BluetoothGattCharacteristic characteristic) {
return enqueue(Request.newEnableIndicationsRequest(characteristic));
}
private boolean internalEnableIndications(final BluetoothGattCharacteristic characteristic) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null || characteristic == null)
return false;
@@ -263,6 +322,10 @@ public class BleManager implements BleProfileApi {
@Override
public final boolean readCharacteristic(final BluetoothGattCharacteristic characteristic) {
return enqueue(Request.newReadRequest(characteristic));
}
private boolean internalReadCharacteristic(final BluetoothGattCharacteristic characteristic) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null || characteristic == null)
return false;
@@ -277,6 +340,10 @@ public class BleManager implements BleProfileApi {
@Override
public final boolean writeCharacteristic(final BluetoothGattCharacteristic characteristic) {
return enqueue(Request.newWriteRequest(characteristic, characteristic.getValue()));
}
private boolean internalWriteCharacteristic(final BluetoothGattCharacteristic characteristic) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null || characteristic == null)
return false;
@@ -289,53 +356,132 @@ public class BleManager implements BleProfileApi {
return gatt.writeCharacteristic(characteristic);
}
public static final class Request {
private enum Type {
WRITE,
READ,
ENABLE_NOTIFICATIONS,
ENABLE_INDICATIONS
}
private final Type type;
private final BluetoothGattCharacteristic characteristic;
private final byte[] value;
private Request(final Type type, final BluetoothGattCharacteristic characteristic) {
this.type = type;
this.characteristic = characteristic;
this.value = null;
}
private Request(final Type type, final BluetoothGattCharacteristic characteristic, final byte[] value) {
this.type = type;
this.characteristic = characteristic;
this.value = value;
}
public static Request newReadRequest(final BluetoothGattCharacteristic characteristic) {
return new Request(Type.READ, characteristic);
}
public static Request newWriteRequest(final BluetoothGattCharacteristic characteristic, final byte[] value) {
return new Request(Type.WRITE, characteristic, value);
}
public static Request newEnableNotificationsRequest(final BluetoothGattCharacteristic characteristic) {
return new Request(Type.ENABLE_NOTIFICATIONS, characteristic);
}
public static Request newEnableIndicationsRequest(final BluetoothGattCharacteristic characteristic) {
return new Request(Type.ENABLE_INDICATIONS, characteristic);
}
@Override
public final boolean readDescriptor(final BluetoothGattDescriptor descriptor) {
return enqueue(Request.newReadRequest(descriptor));
}
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
private Queue<Request> mInitQueue;
private boolean mInitInProgress;
private boolean internalReadDescriptor(final BluetoothGattDescriptor descriptor) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null || descriptor == null)
return false;
private void onError(final String message, final int errorCode) {
mCallbacks.onError(message, errorCode);
return gatt.readDescriptor(descriptor);
}
@Override
public final boolean writeDescriptor(final BluetoothGattDescriptor descriptor) {
return enqueue(Request.newWriteRequest(descriptor, descriptor.getValue()));
}
private boolean internalWriteDescriptor(final BluetoothGattDescriptor descriptor) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null || descriptor == null)
return false;
// There was a bug in Android up to 6.0 where the descriptor was written using parent characteristic write type, instead of always Write With Response,
// as the spec says.
final BluetoothGattCharacteristic parentCharacteristic = descriptor.getCharacteristic();
final int originalWriteType = parentCharacteristic.getWriteType();
parentCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
final boolean result = gatt.writeDescriptor(descriptor);
parentCharacteristic.setWriteType(originalWriteType);
return result;
}
@Override
public final boolean readBatteryLevel() {
return enqueue(Request.newReadBatteryLevelRequest());
}
private boolean internalReadBatteryLevel() {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null)
return false;
final BluetoothGattService batteryService = gatt.getService(BATTERY_SERVICE);
if (batteryService == null)
return false;
final BluetoothGattCharacteristic batteryLevelCharacteristic = batteryService.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC);
if (batteryLevelCharacteristic == null)
return false;
// Check characteristic property
final int properties = batteryLevelCharacteristic.getProperties();
if ((properties & BluetoothGattCharacteristic.PROPERTY_READ) == 0)
return false;
return internalReadCharacteristic(batteryLevelCharacteristic);
}
@Override
public final boolean setBatteryNotifications(final boolean enable) {
if (enable)
return enqueue(Request.newEnableBatteryLevelNotificationsRequest());
else
return enqueue(Request.newDisableBatteryLevelNotificationsRequest());
}
private boolean internalSetBatteryNotifications(final boolean enable) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null) {
return false;
}
final BluetoothGattService batteryService = gatt.getService(BATTERY_SERVICE);
if (batteryService == null)
return false;
final BluetoothGattCharacteristic batteryLevelCharacteristic = batteryService.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC);
if (batteryLevelCharacteristic == null)
return false;
// Check characteristic property
final int properties = batteryLevelCharacteristic.getProperties();
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0)
return false;
gatt.setCharacteristicNotification(batteryLevelCharacteristic, enable);
final BluetoothGattDescriptor descriptor = batteryLevelCharacteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
if (descriptor != null) {
if (enable) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
} else {
descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
}
return gatt.writeDescriptor(descriptor);
}
return false;
}
@Override
public boolean enqueue(final Request request) {
if (mGattCallback != null) {
// Add the new task to the end of the queue
mGattCallback.mTaskQueue.add(request);
mGattCallback.nextRequest();
return true;
}
return false;
}
private final class BleManagerGattCallback extends BluetoothGattCallback {
private final static String ERROR_CONNECTION_STATE_CHANGE = "Error on connection state change";
private final static String ERROR_DISCOVERY_SERVICE = "Error on discovering services";
private final static String ERROR_AUTH_ERROR_WHILE_BONDED = "Phone has lost bonding information";
private final static String ERROR_READ_CHARACTERISTIC = "Error on reading characteristic";
private final static String ERROR_WRITE_CHARACTERISTIC = "Error on writing characteristic";
private final static String ERROR_READ_DESCRIPTOR = "Error on reading descriptor";
private final static String ERROR_WRITE_DESCRIPTOR = "Error on writing descriptor";
private final Queue<Request> mTaskQueue = new LinkedList<>();
private Deque<Request> mInitQueue;
private boolean mInitInProgress;
private boolean mOperationInProgress;
private void onError(final BluetoothDevice device, final String message, final int errorCode) {
mCallbacks.onError(device, message, errorCode);
if (mProfile != null)
mProfile.onError(message, errorCode);
}
@@ -345,7 +491,8 @@ public class BleManager implements BleProfileApi {
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
// Notify the parent activity/service
mConnected = true;
mCallbacks.onDeviceConnected();
mConnectionState = BluetoothGatt.STATE_CONNECTED;
mCallbacks.onDeviceConnected(gatt.getDevice());
/*
* The onConnectionStateChange event is triggered just after the Android connects to a device.
@@ -375,11 +522,13 @@ public class BleManager implements BleProfileApi {
} else {
if (newState == BluetoothProfile.STATE_DISCONNECTED) {
mConnected = false;
mConnectionState = BluetoothGatt.STATE_DISCONNECTED;
mOperationInProgress = true; // no more calls are possible
if (mUserDisconnected) {
mCallbacks.onDeviceDisconnected();
mCallbacks.onDeviceDisconnected(gatt.getDevice());
close();
} else {
mCallbacks.onLinklossOccur();
mCallbacks.onLinklossOccur(gatt.getDevice());
// We are not closing the connection here as the device should try to reconnect automatically.
// This may be only called when the shouldAutoConnect() method returned true.
}
@@ -405,54 +554,93 @@ public class BleManager implements BleProfileApi {
mInitInProgress = true;
mInitQueue = profile.initGatt(gatt);
// When the device is bonded and has Service Changed characteristic, the indications must be enabled first.
// In case this method returns true we have to continue in the onDescriptorWrite callback
if (ensureServiceChangedEnabled(gatt))
return;
// Before we start executing the initialization queue some other tasks need to be done.
if (mInitQueue == null)
mInitQueue = new LinkedList<>();
// We have discovered services, proceed with the initialization queue.
// Note, that operations are added in reverse order to the front of the queue.
// 3. Enable Battery Level notifications if required (if this char. does not exist, this operation will be skipped)
if (mCallbacks.shouldEnableBatteryLevelNotifications(gatt.getDevice()))
mInitQueue.addFirst(Request.newEnableBatteryLevelNotificationsRequest());
// 2. Read Battery Level characteristic (if such does not exist, this will be skipped)
mInitQueue.addFirst(Request.newReadBatteryLevelRequest());
// 1. On devices running Android 4.3-6.0 the Service Changed characteristic needs to be enabled by the app (for bonded devices)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
mInitQueue.addFirst(Request.newEnableServiceChangedIndicationsRequest());
mOperationInProgress = false;
nextRequest();
} else {
mCallbacks.onDeviceNotSupported();
mCallbacks.onDeviceNotSupported(gatt.getDevice());
disconnect();
}
} else {
DebugLogger.e(TAG, "onServicesDiscovered error " + status);
onError(ERROR_DISCOVERY_SERVICE, status);
onError(gatt.getDevice(), ERROR_DISCOVERY_SERVICE, status);
}
}
@Override
public final void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// The value has been read. Notify the profile and proceed with the initialization queue.
mProfile.onCharacteristicRead(gatt, characteristic);
if (isBatteryLevelCharacteristic(characteristic)) {
final int batteryValue = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
mBatteryValue = batteryValue;
mProfile.onBatteryValueReceived(gatt, batteryValue);
} else {
// The value has been read. Notify the profile and proceed with the initialization queue.
mProfile.onCharacteristicRead(gatt, characteristic);
}
mOperationInProgress = false;
nextRequest();
} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
DebugLogger.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
onError(ERROR_AUTH_ERROR_WHILE_BONDED, status);
onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
}
} else {
DebugLogger.e(TAG, "onCharacteristicRead error " + status);
onError(ERROR_READ_CHARACTERISTIC, status);
onError(gatt.getDevice(), ERROR_READ_CHARACTERISTIC, status);
}
}
@Override
public void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
public final void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// The value has been written. Notify the profile and proceed with the initialization queue.
mProfile.onCharacteristicWrite(gatt, characteristic);
mOperationInProgress = false;
nextRequest();
} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
DebugLogger.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
onError(ERROR_AUTH_ERROR_WHILE_BONDED, status);
onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
}
} else {
DebugLogger.e(TAG, "onCharacteristicRead error " + status);
onError(ERROR_READ_CHARACTERISTIC, status);
DebugLogger.e(TAG, "onCharacteristicWrite error " + status);
onError(gatt.getDevice(), ERROR_WRITE_CHARACTERISTIC, status);
}
}
@Override
public void onDescriptorRead(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// The value has been read. Notify the profile and proceed with the initialization queue.
mProfile.onDescriptorRead(gatt, descriptor);
mOperationInProgress = false;
nextRequest();
} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
DebugLogger.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
}
} else {
DebugLogger.e(TAG, "onDescriptorRead error " + status);
onError(gatt.getDevice(), ERROR_READ_DESCRIPTOR, status);
}
}
@@ -461,68 +649,132 @@ public class BleManager implements BleProfileApi {
if (status == BluetoothGatt.GATT_SUCCESS) {
// The value has been written. Notify the profile and proceed with the initialization queue.
mProfile.onDescriptorWrite(gatt, descriptor);
mOperationInProgress = false;
nextRequest();
} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
DebugLogger.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
onError(ERROR_AUTH_ERROR_WHILE_BONDED, status);
onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
}
} else {
DebugLogger.e(TAG, "onDescriptorWrite error " + status);
onError(ERROR_WRITE_DESCRIPTOR, status);
onError(gatt.getDevice(), ERROR_WRITE_DESCRIPTOR, status);
}
}
@Override
public final void onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
final BluetoothGattDescriptor cccd = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
final boolean notifications = cccd == null || cccd.getValue() == null || cccd.getValue().length != 2 || cccd.getValue()[0] == 0x01;
if (isBatteryLevelCharacteristic(characteristic)) {
final int batteryValue = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
mBatteryValue = batteryValue;
mProfile.onBatteryValueReceived(gatt, batteryValue);
} else {
final BluetoothGattDescriptor cccd = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
final boolean notifications = cccd == null || cccd.getValue() == null || cccd.getValue().length != 2 || cccd.getValue()[0] == 0x01;
if (notifications) {
mProfile.onCharacteristicNotified(gatt, characteristic);
} else { // indications
mProfile.onCharacteristicIndicated(gatt, characteristic);
if (notifications) {
mProfile.onCharacteristicNotified(gatt, characteristic);
} else { // indications
mProfile.onCharacteristicIndicated(gatt, characteristic);
}
}
}
/**
* Executes the next initialization request. If the last element from the queue has been executed a {@link BleManagerCallbacks#onDeviceReady()} callback is called.
* Executes the next request. If the last element from the initialization queue has been executed
* the {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} callback is called.
*/
private void nextRequest() {
final Queue<Request> requests = mInitQueue;
if (mOperationInProgress)
return;
// Get the first request from the queue
final Request request = requests != null ? requests.poll() : null;
// Get the first request from the init queue
Request request = mInitQueue != null ? mInitQueue.poll() : null;
// Are we done?
// Are we done with initializing?
if (request == null) {
if (mInitInProgress) {
mInitQueue = null; // release the queue
mInitInProgress = false;
mCallbacks.onDeviceReady();
mCallbacks.onDeviceReady(mBluetoothDevice);
}
// If so, we can continue with the task queue
request = mTaskQueue.poll();
if (request == null) {
// Nothing to be done for now
return;
}
return;
}
mOperationInProgress = true;
boolean result = false;
switch (request.type) {
case READ: {
readCharacteristic(request.characteristic);
result = internalReadCharacteristic(request.characteristic);
break;
}
case WRITE: {
final BluetoothGattCharacteristic characteristic = request.characteristic;
characteristic.setValue(request.value);
writeCharacteristic(characteristic);
characteristic.setWriteType(request.writeType);
result = internalWriteCharacteristic(characteristic);
break;
}
case READ_DESCRIPTOR: {
result = internalReadDescriptor(request.descriptor);
break;
}
case WRITE_DESCRIPTOR: {
final BluetoothGattDescriptor descriptor = request.descriptor;
descriptor.setValue(request.value);
result = internalWriteDescriptor(descriptor);
break;
}
case ENABLE_NOTIFICATIONS: {
enableNotifications(request.characteristic);
result = internalEnableNotifications(request.characteristic);
break;
}
case ENABLE_INDICATIONS: {
enableIndications(request.characteristic);
result = internalEnableIndications(request.characteristic);
break;
}
case READ_BATTERY_LEVEL: {
result = internalReadBatteryLevel();
break;
}
case ENABLE_BATTERY_LEVEL_NOTIFICATIONS: {
result = internalSetBatteryNotifications(true);
break;
}
case DISABLE_BATTERY_LEVEL_NOTIFICATIONS: {
result = internalSetBatteryNotifications(false);
break;
}
case ENABLE_SERVICE_CHANGED_INDICATIONS: {
result = ensureServiceChangedEnabled();
break;
}
}
// The result may be false if given characteristic or descriptor were not found on the device.
// In that case, proceed with next operation and ignore the one that failed.
if (!result) {
mOperationInProgress = false;
nextRequest();
}
}
};
/**
* Returns true if the characteristic is the Battery Level characteristic.
*
* @param characteristic the characteristic to be checked
* @return true if the characteristic is the Battery Level characteristic.
*/
private boolean isBatteryLevelCharacteristic(final BluetoothGattCharacteristic characteristic) {
if (characteristic == null)
return false;
return BATTERY_LEVEL_CHARACTERISTIC.equals(characteristic.getUuid());
}
}
}

View File

@@ -21,60 +21,91 @@
*/
package no.nordicsemi.android.nrftoolbox.ble;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
public interface BleManagerCallbacks {
/**
* Called when the device has been connected. This does not mean that the application may start communication. A service discovery will be handled automatically after this call. Service discovery
* may ends up with calling {@link #onDeviceReady()} or {@link #onDeviceNotSupported()} if required services have not been found.
* Called when the Android device started connecting to given device.
* The {@link #onDeviceConnected(BluetoothDevice)} will be called when the device is connected,
* or {@link #onError(BluetoothDevice, String, int)} in case of error.
* @param device the device that got connected
*/
public void onDeviceConnected();
void onDeviceConnecting(final BluetoothDevice device);
/**
* Called when the device has been connected. This does not mean that the application may start communication.
* A service discovery will be handled automatically after this call. Service discovery
* may ends up with calling {@link #onDeviceReady(BluetoothDevice)}
* or {@link #onDeviceNotSupported(BluetoothDevice)} if required services have not been found.
* @param device target device
*/
void onDeviceConnected(final BluetoothDevice device);
/**
* Method called when all initialization requests has been completed.
* @param device target device
*/
public void onDeviceReady();
void onDeviceReady(final BluetoothDevice device);
/**
* This method should return true if Battery Level notifications should be enabled on the target device.
* If there is no Battery Service, or the Battery Level characteristic does not have NOTIFY property,
* this method will not be called for this device.
* <p>This method may return true only if an activity is bound to the service (to display the information
* to the user), always (e.g. if critical battery level is reported using notifications) or never, if
* such information is not important or the manager wants to control Battery Level notifications on its own.</p>
* @param device target device
* @return true to enabled battery level notifications after connecting to the device, false otherwise
*/
boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device);
/**
* Called when user initialized disconnection.
* @param device target device
*/
public void onDeviceDisconnecting();
void onDeviceDisconnecting(final BluetoothDevice device);
/**
* Called when the device has disconnected (when the callback returned {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)} with state DISCONNECTED.
* Called when the device has disconnected (when the callback returned
* {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)} with state DISCONNECTED.
* @param device target device
*/
public void onDeviceDisconnected();
void onDeviceDisconnected(final BluetoothDevice device);
/**
* This callback is invoked when the Ble Manager lost connection to a device that has been connected with autoConnect option. Otherwise a {@link #onDeviceDisconnected()}
* method will be called on such event.
* This callback is invoked when the Ble Manager lost connection to a device that has been connected with autoConnect option.
* Otherwise a {@link #onDeviceDisconnected(BluetoothDevice)} method will be called on such event.
* @param device target device
*/
public void onLinklossOccur();
void onLinklossOccur(final BluetoothDevice device);
/**
* Called when an {@link BluetoothGatt#GATT_INSUFFICIENT_AUTHENTICATION} error occurred and the device bond state is NOT_BONDED
* @param device target device
*/
public void onBondingRequired();
void onBondingRequired(final BluetoothDevice device);
/**
* Called when the device has been successfully bonded.
* @param device target device
*/
public void onBonded();
void onBonded(final BluetoothDevice device);
/**
* Called when a BLE error has occurred
*
* @param message
* the error message
* @param errorCode
* the error code
*
* @param device target device
* @param message the error message
* @param errorCode the error code
*/
public void onError(final String message, final int errorCode);
void onError(final BluetoothDevice device, final String message, final int errorCode);
/**
* Called when service discovery has finished but the main services were not found on the device.
* @param device target device
*/
public void onDeviceNotSupported();
void onDeviceNotSupported(final BluetoothDevice device);
}

View File

@@ -27,7 +27,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.Context;
import java.util.Queue;
import java.util.Deque;
public abstract class BleProfile {
private Context mContext;
@@ -63,19 +63,28 @@ public abstract class BleProfile {
* @param gatt the gatt device with services discovered
* @return the queue of requests
*/
protected abstract Queue<BleManager.Request> initGatt(final BluetoothGatt gatt);
protected abstract Deque<BleManager.Request> initGatt(final BluetoothGatt gatt);
/**
* Releases all profile resources. The device is no longer connected.
*/
protected abstract void release();
/**
* Called when battery value has been received from the device.
*
* @param gatt GATT client
* @param value the battery value in percent
*/
protected void onBatteryValueReceived(final BluetoothGatt gatt, final int value) {
// do nothing
}
/**
* Callback reporting the result of a characteristic read operation.
*
* @param gatt GATT client invoked {@link BluetoothGatt#readCharacteristic}
* @param characteristic Characteristic that was read from the associated
* remote device.
* @param gatt GATT client
* @param characteristic Characteristic that was read from the associated remote device.
*/
protected void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
// do nothing
@@ -83,40 +92,65 @@ public abstract class BleProfile {
/**
* Callback indicating the result of a characteristic write operation.
* <p/>
* <p>If this callback is invoked while a reliable write transaction is
* in progress, the value of the characteristic represents the value
* reported by the remote device. An application should compare this
* value to the desired value to be written. If the values don't match,
* the application must abort the reliable write transaction.
*
* @param gatt GATT client invoked {@link BluetoothGatt#writeCharacteristic}
* @param characteristic Characteristic that was written to the associated
* remote device.
* @param gatt GATT client
* @param characteristic Characteristic that was written to the associated remote device.
*/
protected void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
// do nothing
}
/**
* Callback reporting the result of a descriptor read operation.
*
* @param gatt GATT client
* @param descriptor Descriptor that was read from the associated remote device.
*/
protected void onDescriptorRead(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor) {
// do nothing
}
/**
* Callback indicating the result of a descriptor write operation.
* <p>If this callback is invoked while a reliable write transaction is in progress,
* the value of the characteristic represents the value reported by the remote device.
* An application should compare this value to the desired value to be written.
* If the values don't match, the application must abort the reliable write transaction.
*
* @param gatt GATT client
* @param descriptor Descriptor that was written to the associated remote device.
*/
protected void onDescriptorWrite(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor) {
// do nothing
}
/**
* Callback indicating a notification has been received.
* @param gatt GATT client
* @param characteristic Characteristic from which the notification came.
*/
protected void onCharacteristicNotified(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
// do nothing
}
/**
* Callback indicating an indication has been received.
* @param gatt GATT client
* @param characteristic Characteristic from which the indication came.
*/
protected void onCharacteristicIndicated(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
// do nothing
}
/**
* Called when a BLE error has occurred
*
* @param message
* the error message
* @param errorCode
* the error code
* @param message the error message
* @param errorCode the error code
*/
public void onError(final String message, final int errorCode) {
// do nothing

View File

@@ -23,41 +23,312 @@
package no.nordicsemi.android.nrftoolbox.ble;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.Context;
public interface BleProfileApi {
/**
* On Android, when multiple BLE operations needs to be done, it is required to wait for a proper
* {@link android.bluetooth.BluetoothGattCallback BluetoothGattCallback} callback before calling
* another operation. In order to make BLE operations easier the BleManager allows to enqueue a request
* containing all data necessary for a given operation. Requests are performed one after another until the
* queue is empty. Use static methods from below to instantiate a request and then enqueue them using {@link #enqueue(Request)}.
*/
final class Request {
enum Type {
WRITE,
READ,
WRITE_DESCRIPTOR,
READ_DESCRIPTOR,
ENABLE_NOTIFICATIONS,
ENABLE_INDICATIONS,
READ_BATTERY_LEVEL,
ENABLE_BATTERY_LEVEL_NOTIFICATIONS,
DISABLE_BATTERY_LEVEL_NOTIFICATIONS,
ENABLE_SERVICE_CHANGED_INDICATIONS,
}
final Type type;
final BluetoothGattCharacteristic characteristic;
final BluetoothGattDescriptor descriptor;
final byte[] value;
final int writeType;
private Request(final Type type) {
this.type = type;
this.characteristic = null;
this.descriptor = null;
this.value = null;
this.writeType = 0;
}
private Request(final Type type, final BluetoothGattCharacteristic characteristic) {
this.type = type;
this.characteristic = characteristic;
this.descriptor = null;
this.value = null;
this.writeType = 0;
}
private Request(final Type type, final BluetoothGattCharacteristic characteristic, final int writeType, final byte[] value, final int offset, final int length) {
this.type = type;
this.characteristic = characteristic;
this.descriptor = null;
this.value = copy(value, offset, length);
this.writeType = writeType;
}
private Request(final Type type, final BluetoothGattDescriptor descriptor) {
this.type = type;
this.characteristic = null;
this.descriptor = descriptor;
this.value = null;
this.writeType = 0;
}
private Request(final Type type, final BluetoothGattDescriptor descriptor, final byte[] value, final int offset, final int length) {
this.type = type;
this.characteristic = null;
this.descriptor = descriptor;
this.value = copy(value, offset, length);
this.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
}
private static byte[] copy(final byte[] value, final int offset, final int length) {
if (value == null || offset > value.length)
return null;
final int maxLength = Math.min(value.length - offset, length);
final byte[] copy = new byte[maxLength];
System.arraycopy(value, offset, copy, 0, maxLength);
return copy;
}
/**
* Creates new Read Characteristic request. The request will not be executed if given characteristic
* is null or does not have READ property. After the operation is complete a proper callback will be invoked.
* @param characteristic characteristic to be read
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newReadRequest(final BluetoothGattCharacteristic characteristic) {
return new Request(Type.READ, characteristic);
}
/**
* Creates new Write Characteristic request. The request will not be executed if given characteristic
* is null or does not have WRITE property. After the operation is complete a proper callback will be invoked.
* @param characteristic characteristic to be written
* @param value value to be written. The array is copied into another buffer so it's safe to reuse the array again.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newWriteRequest(final BluetoothGattCharacteristic characteristic, final byte[] value) {
return new Request(Type.WRITE, characteristic, characteristic.getWriteType(), value, 0, value != null ? value.length : 0);
}
/**
* Creates new Write Characteristic request. The request will not be executed if given characteristic
* is null or does not have WRITE property. After the operation is complete a proper callback will be invoked.
* @param characteristic characteristic to be written
* @param value value to be written. The array is copied into another buffer so it's safe to reuse the array again.
* @param writeType write type to be used, one of {@link BluetoothGattCharacteristic#WRITE_TYPE_DEFAULT}, {@link BluetoothGattCharacteristic#WRITE_TYPE_NO_RESPONSE}.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newWriteRequest(final BluetoothGattCharacteristic characteristic, final byte[] value, final int writeType) {
return new Request(Type.WRITE, characteristic, writeType, value, 0, value != null ? value.length : 0);
}
/**
* Creates new Write Characteristic request. The request will not be executed if given characteristic
* is null or does not have WRITE property. After the operation is complete a proper callback will be invoked.
* @param characteristic characteristic to be written
* @param value value to be written. The array is copied into another buffer so it's safe to reuse the array again.
* @param offset the offset from which value has to be copied
* @param length number of bytes to be copied from the value buffer
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newWriteRequest(final BluetoothGattCharacteristic characteristic, final byte[] value, final int offset, final int length) {
return new Request(Type.WRITE, characteristic, characteristic.getWriteType(), value, offset, length);
}
/**
* Creates new Write Characteristic request. The request will not be executed if given characteristic
* is null or does not have WRITE property. After the operation is complete a proper callback will be invoked.
* @param characteristic characteristic to be written
* @param value value to be written. The array is copied into another buffer so it's safe to reuse the array again.
* @param offset the offset from which value has to be copied
* @param length number of bytes to be copied from the value buffer
* @param writeType write type to be used, one of {@link BluetoothGattCharacteristic#WRITE_TYPE_DEFAULT}, {@link BluetoothGattCharacteristic#WRITE_TYPE_NO_RESPONSE}.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newWriteRequest(final BluetoothGattCharacteristic characteristic, final byte[] value, final int offset, final int length, final int writeType) {
return new Request(Type.WRITE, characteristic, writeType, value, offset, length);
}
/**
* Creates new Read Descriptor request. The request will not be executed if given descriptor
* is null. After the operation is complete a proper callback will be invoked.
* @param descriptor descriptor to be read
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newReadRequest(final BluetoothGattDescriptor descriptor) {
return new Request(Type.READ_DESCRIPTOR, descriptor);
}
/**
* Creates new Write Descriptor request. The request will not be executed if given descriptor
* is null. After the operation is complete a proper callback will be invoked.
* @param descriptor descriptor to be written
* @param value value to be written. The array is copied into another buffer so it's safe to reuse the array again.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newWriteRequest(final BluetoothGattDescriptor descriptor, final byte[] value) {
return new Request(Type.WRITE_DESCRIPTOR, descriptor, value, 0, value != null ? value.length : 0);
}
/**
* Creates new Write Descriptor request. The request will not be executed if given descriptor
* is null. After the operation is complete a proper callback will be invoked.
* @param descriptor descriptor to be written
* @param value value to be written. The array is copied into another buffer so it's safe to reuse the array again.
* @param offset the offset from which value has to be copied
* @param length number of bytes to be copied from the value buffer
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newWriteRequest(final BluetoothGattDescriptor descriptor, final byte[] value, final int offset, final int length) {
return new Request(Type.WRITE_DESCRIPTOR, descriptor, value, offset, length);
}
/**
* Creates new Enable Notification request. The request will not be executed if given characteristic
* is null, does not have NOTIFY property or the CCCD. After the operation is complete a proper callback will be invoked.
* @param characteristic characteristic to have notifications enabled
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newEnableNotificationsRequest(final BluetoothGattCharacteristic characteristic) {
return new Request(Type.ENABLE_NOTIFICATIONS, characteristic);
}
/**
* Creates new Enable Indications request. The request will not be executed if given characteristic
* is null, does not have INDICATE property or the CCCD. After the operation is complete a proper callback will be invoked.
* @param characteristic characteristic to have indications enabled
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newEnableIndicationsRequest(final BluetoothGattCharacteristic characteristic) {
return new Request(Type.ENABLE_INDICATIONS, characteristic);
}
/**
* Reads the first found Battery Level characteristic value from the first found Battery Service.
* If any of them is not found, or the characteristic does not have the READ property this operation will not execute.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newReadBatteryLevelRequest() {
return new Request(Type.READ_BATTERY_LEVEL); // the first Battery Level char from the first Battery Service is used
}
/**
* Enables notifications on the first found Battery Level characteristic from the first found Battery Service.
* If any of them is not found, or the characteristic does not have the NOTIFY property this operation will not execute.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newEnableBatteryLevelNotificationsRequest() {
return new Request(Type.ENABLE_BATTERY_LEVEL_NOTIFICATIONS); // the first Battery Level char from the first Battery Service is used
}
/**
* Disables notifications on the first found Battery Level characteristic from the first found Battery Service.
* If any of them is not found, or the characteristic does not have the NOTIFY property this operation will not execute.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
public static Request newDisableBatteryLevelNotificationsRequest() {
return new Request(Type.DISABLE_BATTERY_LEVEL_NOTIFICATIONS); // the first Battery Level char from the first Battery Service is used
}
/**
* Enables indications on Service Changed characteristic if such exists in the Generic Attribute service.
* It is required to enable those notifications on bonded devices on older Android versions to be
* informed about attributes changes. Android 7+ (or 6+) handles this automatically and no action is required.
* @return the new request that can be enqueued using {@link #enqueue(Request)} method.
*/
static Request newEnableServiceChangedIndicationsRequest() {
return new Request(Type.ENABLE_SERVICE_CHANGED_INDICATIONS); // the only Service Changed char is used (if such exists)
}
}
/**
* Returns the context.
*/
public Context getContext();
Context getContext();
/**
* Enables notifications on given characteristic
*
* @return true is the request has been sent, false if one of the arguments was <code>null</code> or the characteristic does not have the CCCD.
* @return true is the request has been enqueued
*/
public boolean enableNotifications(final BluetoothGattCharacteristic characteristic);
boolean enableNotifications(final BluetoothGattCharacteristic characteristic);
/**
* Enables indications on given characteristic
*
* @return true is the request has been sent, false if one of the arguments was <code>null</code> or the characteristic does not have the CCCD.
* @return true is the request has been enqueued
*/
public boolean enableIndications(final BluetoothGattCharacteristic characteristic);
boolean enableIndications(final BluetoothGattCharacteristic characteristic);
/**
* Sends the read request to the given characteristic.
*
* @param characteristic the characteristic to read
* @return true if request has been sent
* @return true if request has been enqueued
*/
public boolean readCharacteristic(final BluetoothGattCharacteristic characteristic);
boolean readCharacteristic(final BluetoothGattCharacteristic characteristic);
/**
* Writes the characteristic value to the given characteristic.
*
* @param characteristic the characteristic to write to
* @return true if request has been sent
* @return true if request has been enqueued
*/
public boolean writeCharacteristic(final BluetoothGattCharacteristic characteristic);
boolean writeCharacteristic(final BluetoothGattCharacteristic characteristic);
/**
* Sends the read request to the given descriptor.
*
* @param descriptor the descriptor to read
* @return true if request has been enqueued
*/
boolean readDescriptor(final BluetoothGattDescriptor descriptor);
/**
* Writes the descriptor value to the given descriptor.
*
* @param descriptor the descriptor to write to
* @return true if request has been enqueued
*/
boolean writeDescriptor(final BluetoothGattDescriptor descriptor);
/**
* Reads the battery level from the device.
*
* @return true if request has been enqueued
*/
boolean readBatteryLevel();
/**
* This method tries to enable notifications on the Battery Level characteristic.
*
* @param enable <code>true</code> to enable battery notifications, false to disable
* @return true if request has been enqueued
*/
boolean setBatteryNotifications(final boolean enable);
/**
* Enqueues a new request. The request will be handled immediately if there is no operation in progress,
* or automatically after the last enqueued one will finish.
* <p>This method should be used to read and write data from the target device as it ensures that the last operation has finished
* before a new one will be called.</p>
* @param request new request to be performed
* @return true if request has been enqueued, false if the device is not connected
*/
boolean enqueue(final Request request);
}

View File

@@ -25,6 +25,8 @@ import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
@@ -41,12 +43,14 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
public static final String BROADCAST_DEVICE_NOT_SUPPORTED = "no.nordicsemi.android.nrftoolbox.BROADCAST_DEVICE_NOT_SUPPORTED";
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";
/** 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_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_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE";
@@ -63,17 +67,34 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
protected boolean mBinded;
private boolean mConnected;
private String mDeviceAddress;
private BluetoothDevice mBluetoothDevice;
private String mDeviceName;
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);
switch (state) {
case BluetoothAdapter.STATE_ON:
onBluetoothEnabled();
break;
case BluetoothAdapter.STATE_TURNING_OFF:
case BluetoothAdapter.STATE_OFF:
onBluetoothDisabled();
break;
}
}
};
public class LocalBinder extends Binder {
/**
* Disconnects from the sensor.
*/
public final void disconnect() {
public void disconnect() {
if (!mConnected) {
mBleManager.close();
onDeviceDisconnected();
onDeviceDisconnected(mBluetoothDevice);
return;
}
@@ -85,8 +106,8 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
*
* @return device address
*/
public String getDeviceAddress() {
return mDeviceAddress;
public final String getDeviceAddress() {
return mBluetoothDevice.getAddress();
}
/**
@@ -94,29 +115,38 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
*
* @return the device name
*/
public String getDeviceName() {
public final String getDeviceName() {
return mDeviceName;
}
/**
* Returns the Bluetooth device
*
* @return the Bluetooth device
*/
public final BluetoothDevice getBluetoothDevice() {
return mBluetoothDevice;
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
*
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
public boolean isConnected() {
public final boolean isConnected() {
return mConnected;
}
/**
* Returns the Profile API. Profile may be null if service discovery has not been performed or the device does not match any profile.
*/
public BleProfile getProfile() {
public final BleProfile getProfile() {
return mBleManager.getProfile();
}
}
/**
* Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the binded activity.
* 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
*/
@@ -140,7 +170,7 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
public final boolean onUnbind(final Intent intent) {
mBinded = false;
// we must allow to rebind to the same service
// We want the onRebind method be called if anything else binds to it again
return true;
}
@@ -153,6 +183,25 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
// initialize the manager
mBleManager = new BleManager(this, 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
}
@Override
@@ -160,58 +209,92 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS))
throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key");
mDeviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
// notify user about changing the state to CONNECTING
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING);
LocalBroadcastManager.getInstance(BleProfileService.this).sendBroadcast(broadcast);
mDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME);
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
final BluetoothAdapter adapter = bluetoothManager.getAdapter();
final BluetoothDevice device = adapter.getRemoteDevice(mDeviceAddress);
mDeviceName = device.getName();
final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
mBluetoothDevice = adapter.getRemoteDevice(deviceAddress);
onServiceStarted();
mBleManager.connect(device);
mBleManager.connect(mBluetoothDevice);
return START_REDELIVER_INTENT;
}
/**
* Called when the service has been started. The device name and address are set. It nRF Logger is installed than logger was also initialized.
*/
protected void onServiceStarted() {
// empty default implementation
}
@Override
public void onDestroy() {
super.onDestroy();
// Unregister broadcast receivers
unregisterReceiver(mBluetoothStateBroadcastReceiver);
// shutdown the manager
mBleManager.close();
mBleManager = null;
mDeviceAddress = null;
mBluetoothDevice = null;
mDeviceName = null;
mConnected = false;
}
/**
* 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 onDeviceConnected() {
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, mBluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceConnected(final BluetoothDevice device) {
mConnected = true;
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED);
broadcast.putExtra(EXTRA_DEVICE_ADDRESS, mDeviceAddress);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_DEVICE_NAME, mDeviceName);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceDisconnecting() {
public void onDeviceDisconnecting(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);
}
@Override
public void onDeviceDisconnected() {
public void onDeviceDisconnected(final BluetoothDevice device) {
mConnected = false;
mDeviceAddress = null;
mDeviceName = null;
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED);
@@ -221,53 +304,61 @@ public class BleProfileService extends Service implements BleManagerCallbacks {
}
@Override
public void onLinklossOccur() {
public void onLinklossOccur(final BluetoothDevice device) {
mConnected = false;
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 onDeviceReady() {
public void onDeviceReady(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() {
public void onDeviceNotSupported(final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_DEVICE_NOT_SUPPORTED);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
// no need for disconnecting, it will be disconnected by the manager automatically
}
@Override
public void onBondingRequired() {
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, mBluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBonded() {
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, mBluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onError(final String message, final int errorCode) {
public void onError(final BluetoothDevice device, 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);
// After receiving an error the device will be automatically disconnected.
// Replace it with other implementation if necessary.
mBleManager.disconnect();
stopSelf();
}

View File

@@ -27,11 +27,12 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.text.TextUtils;
import java.util.Queue;
import java.util.Deque;
import java.util.UUID;
import no.nordicsemi.android.nrftoolbox.ble.BleManager;
import no.nordicsemi.android.nrftoolbox.ble.BleProfile;
import no.nordicsemi.android.nrftoolbox.ble.BleProfileApi;
public class UARTProfile extends BleProfile {
/** Broadcast sent when a UART message is received. */
@@ -40,11 +41,11 @@ public class UARTProfile extends BleProfile {
public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.EXTRA_DATA";
/** Nordic UART Service UUID */
private final static UUID UART_SERVICE_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
private static final UUID UART_SERVICE_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
/** RX characteristic UUID */
private final static UUID UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
private static final UUID UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
/** TX characteristic UUID */
private final static UUID UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E");
private static final UUID UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E");
/** The maximum packet size is 20 bytes. */
private static final int MAX_PACKET_SIZE = 20;
@@ -64,7 +65,7 @@ public class UARTProfile extends BleProfile {
private int mBufferOffset;
@Override
protected Queue<BleManager.Request> initGatt(final BluetoothGatt gatt) {
protected Deque<BleManager.Request> initGatt(final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(UART_SERVICE_UUID);
mTXCharacteristic = service.getCharacteristic(UART_TX_CHARACTERISTIC_UUID);
mRXCharacteristic = service.getCharacteristic(UART_RX_CHARACTERISTIC_UUID);
@@ -78,8 +79,8 @@ public class UARTProfile extends BleProfile {
mRXCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
// We don't want to enable notifications on TX characteristic as we are not showing them here. A watch may be just used to send data. At least now.
// final LinkedList<BleManager.Request> requests = new LinkedList<>();
// requests.add(BleManager.Request.newEnableNotificationsRequest(mTXCharacteristic));
// final LinkedList<BleProfileApi.Request> requests = new LinkedList<>();
// requests.add(BleProfileApi.Request.newEnableNotificationsRequest(mTXCharacteristic));
// return requests;
return null;
}
@@ -106,11 +107,8 @@ public class UARTProfile extends BleProfile {
mOutgoingBuffer = null;
} else { // Otherwise...
final int length = Math.min(buffer.length - mBufferOffset, MAX_PACKET_SIZE);
final byte[] data = new byte[length]; // We send at most 20 bytes
System.arraycopy(buffer, mBufferOffset, data, 0, length);
getApi().enqueue(BleProfileApi.Request.newWriteRequest(mRXCharacteristic, buffer, mBufferOffset, length));
mBufferOffset += length;
mRXCharacteristic.setValue(data);
getApi().writeCharacteristic(mRXCharacteristic);
}
}
@@ -134,15 +132,12 @@ public class UARTProfile extends BleProfile {
if (!writeRequest) { // no WRITE REQUEST property
final int length = Math.min(buffer.length, MAX_PACKET_SIZE);
final byte[] data = new byte[length]; // We send at most 20 bytes
System.arraycopy(buffer, 0, data, 0, length);
mBufferOffset += length;
mRXCharacteristic.setValue(data);
} else { // there is WRITE REQUEST property
mRXCharacteristic.setValue(buffer);
getApi().enqueue(BleProfileApi.Request.newWriteRequest(mRXCharacteristic, buffer, 0, length));
} else { // there is WRITE REQUEST property, let's try Long Write
mBufferOffset = buffer.length;
getApi().enqueue(BleProfileApi.Request.newWriteRequest(mRXCharacteristic, buffer, 0, buffer.length));
}
getApi().writeCharacteristic(mRXCharacteristic);
}
}
}