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:

  1. Disabling BT: Disconnecting some important device, like heart rate monitor
  2. Enabling BT: Automatically connecting some BT peripheral like loudspeaker and blasting sound from hardcore porn private audible information user was currently enjoying all around.
  3. 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:

  1. Don’t disable BT if user has changed anything since our enabling of BT. This requires tracking state of BT on BroadcastReceiver.
  2. Don’t enable BT if user just enabled it.
  3. 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.