Bluetooth inside a geofence
Background
The primary scenario is turning BT on for some time to scan when user enters geofence, then turn it off back again. In general turning user’s radios on/off is not a desirable thing to do. This may cause for example:
- Disabling BT: Disconnecting some important device, like heart rate monitor
- Enabling BT: Automatically connecting some BT peripheral like loudspeaker and transferring any audio that the user enjoyed privately to it.
- Any case where we’re changing BT status might be confusing to the user if he sees it. (“Huh, I’m pretty sure I had BT off…”) On the other hand turning BT on/off might give us a chance to deliver better suited information to the user
Damage control
Steps we can take to avoid most of the issues above with changing BT state:
- Don’t disable BT if user has changed anything since our enabling of BT. This requires tracking state of BT on BroadcastReceiver.
- Don’t enable BT if user just enabled it.
- Do this only when in background, since user is most likely not present and won’t change BT state then. (This is not entirely avoidable - user can check or toggle BT state even when phone is locked)
Solution
This receiver role is to:
- Listen to any BT status changes and save timestamp of the change to SharedPreferences
- Enable / disable BT and save timestamp to SharedPreferences
- Schedule disabling BT after given timeout.
Note: All operations of writing to SharedPreferences should happen on the same thread. Hence enableBluetooth / disableBluetooth methods are not directly turning BT on/off, but rather send intent to itself via sendBroadcast so all timestamp saving happens on main thread.
BluetoothReceiver.java
public class BluetoothReceiver extends BroadcastReceiver {
public static final String ACTION_DISABLE_BLUETOOTH = "com.sensorberg.android.ACTION_DISABLE_BLUETOOTH";
public static final String ACTION_ENABLE_BLUETOOTH = "com.sensorberg.android.ACTION_ENABLE_BLUETOOTH";
public static final String EXTRA_TIMEOUT = "extra_timeout";
public static final long TIME_TO_ENABLE_BLUETOOTH = 5 * TimeConstants.ONE_SECOND;
private static final String TAG = MyActionReceiver.class.getName();
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
BluetoothAdapter adapter = Bluetooth.provideAdapter(context);
String action = intent.getAction();
if (action == null) {
return;
}
switch (action) {
case BluetoothAdapter.ACTION_STATE_CHANGED:
//Save current timestamp for every BT state change
Bluetooth.setLastBtChange(prefs);
break;
case ACTION_DISABLE_BLUETOOTH:
long lastBtChange = Bluetooth.getLastBtChange(prefs);
long ourBtEnable = Bluetooth.getLastOurBtEnable(prefs);
//Disable only if there were no user BT interaction since we've enabled it.
if (lastBtChange - ourBtEnable < TIME_TO_ENABLE_BLUETOOTH && Bluetooth.isBluetoothEnabled(adapter)) {
//This also saves timestamp of our BT enabling
Bluetooth.setBluetooth(adapter, prefs, false);
}
break;
case ACTION_ENABLE_BLUETOOTH:
long timeout = intent.getLongExtra(EXTRA_TIMEOUT, 0);
if (Bluetooth.isBluetoothOff(adapter)) {
//If it's STATE_TURNING_OFF we resign.
long now = System.currentTimeMillis();
Bluetooth.setBluetooth(adapter, prefs, true);
scheduleBluetoothOff(context, timeout, now);
}
break;
default:
Log.w(TAG, "Unknown action: " + action);
break;
}
}
//This is called from MyActionReceiver which runs on Sensorberg process,
//so we send ourselves an Intent to bring all operations to main thread.
public static void enableBluetooth(Context context, long timeout) {
Intent intent = new Intent(context, BluetoothReceiver.class);
intent.setAction(ACTION_ENABLE_BLUETOOTH);
intent.putExtra(EXTRA_TIMEOUT, timeout);
context.sendBroadcast(intent);
}
//This is called from MyActionReceiver which runs on Sensorberg process,
//so we send ourselves an Intent to bring all operations to main thread.
public static void disableBluetooth(Context context) {
Intent intent = new Intent(context, BluetoothReceiver.class);
intent.setAction(ACTION_DISABLE_BLUETOOTH);
context.sendBroadcast(intent);
}
public static void scheduleBluetoothOff(Context context, long timeout, long now) {
if (timeout > 0) {
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, BluetoothReceiver.class);
intent.setAction(ACTION_DISABLE_BLUETOOTH);
PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarm.set(AlarmManager.RTC_WAKEUP, now + timeout, pending);
}
}
}
The receiver should be obviously registered in AndroidManifest.xml:
AndroidManifest.xml
<receiver android:name=".receivers.BluetoothReceiver">
<intent-filter>
<action android:name="com.sensorberg.android.ACTION_DISABLE_BLUETOOTH"/>
<action android:name="com.sensorberg.android.ACTION_ENABLE_BLUETOOTH"/>
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED"/>
</intent-filter>
</receiver>
The source of actions is the ActionReceiver, so we only do it when app is in background.
MyActionReceiver.java
public class MyActionReceiver extends ActionReceiver {
@Override
public void onAction(Action action, BeaconId beaconId, Context context) {
onActionPayload(context, action.getPayload());
}
private void onActionPayload(Context context, String payload) {
if (payload == null || payload.isEmpty()) {
return;
}
try {
JSONObject json = new JSONObject(payload);
if (!json.has("enable_bluetooth")) {
return;
}
boolean enable = json.getBoolean("enable_bluetooth");
long timeout = 0; //Disregard for disable
try {
timeout = json.getLong("disable_bluetooth_after");
} catch (JSONException ex) {
/*Swallow*/
}
if (enable) {
BluetoothReceiver.enableBluetooth(context, timeout);
} else {
BluetoothReceiver.disableBluetooth(context);
}
} catch (JSONException ex) {
Log.w(TAG, "Json payload issue", ex);
}
}
}
The payload that triggers BT changes:
Json payload to enable BT for 60 seconds
{
"enable_bluetooth": true,
"disable_bluetooth_after": 60000
}
Disabling is done only if user hasn’t change BT state in period between our enable and following 60 seconds, and only if the BT was off before. In case disable_bluetooth_after is not given we’re not scheduling BT off.
Disabling can then be done by simply using exit geofence notification payload:
Json payload to disable BT
{
"enable_bluetooth": false
}
For this case the disable_bluetooth_after field is ignored.
The helper method for BT operations and saving timestamps:
Bluetooth.java
public class Bluetooth {
private static final String KEY_LAST_OUR_BT_ENABLE = "last_our_bluetooth_enable";
private static final String KEY_LAST_BT_CHANGE = "last_bluetooth_change";
public static boolean isBluetoothEnabled(BluetoothAdapter adapter) {
return adapter != null && adapter.isEnabled();
}
public static boolean isBluetoothOff(BluetoothAdapter adapter) {
return adapter == null || adapter.getState() == BluetoothAdapter.STATE_OFF;
}
public static void setBluetooth(BluetoothAdapter adapter, SharedPreferences prefs, boolean enable) {
if (adapter == null) {
return;
}
if (enable) {
long now = System.currentTimeMillis();
adapter.enable();
prefs.edit().putLong(KEY_LAST_OUR_BT_ENABLE, now).apply();
} else {
adapter.disable();
}
}
public static long getLastOurBtEnable(SharedPreferences prefs) {
return prefs.getLong(KEY_LAST_OUR_BT_ENABLE, 0);
}
public static void setLastBtChange(SharedPreferences prefs) {
prefs.edit().putLong(KEY_LAST_BT_CHANGE, System.currentTimeMillis()).apply();
}
public static long getLastBtChange(SharedPreferences prefs) {
return prefs.getLong(KEY_LAST_BT_CHANGE, System.currentTimeMillis());
}
}
In case you really need to turn BT on while app is in foreground just make MyActionReceiver.onActionPayload method static and call it wherever you receive Action in foreground.