Permalink
Browse files

Use MAC address from settings if it is unavailable any other way

This change will use the Bluetooth MAC address from settings if needed
and prompt for it if it does not exist. To make this possible, it also
cleans up the flow of checking if BluetoothSyncService can run.

References #4
  • Loading branch information...
aarmea committed Apr 1, 2018
1 parent 56011b8 commit 17d37c380ec4f093821db421ab77d96fc5683667
@@ -17,27 +17,32 @@
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelUuid;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
import net.vidageek.mirror.dsl.Mirror;
import com.alternativeinfrastructures.noise.R;
import com.alternativeinfrastructures.noise.sync.StreamSync;
import com.alternativeinfrastructures.noise.views.SettingsActivity;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public class BluetoothSyncService extends Service {
public static final String TAG = "BluetoothSyncService";
public static final UUID SERVICE_UUID_HALF = UUID.fromString("5ac825f4-6084-42a6-0000-000000000000");
private static final String FAKE_MAC_ADDRESS = "02:00:00:00:00:00";
private static final Pattern MAC_PATTERN = Pattern.compile("\\w\\w:\\w\\w:\\w\\w:\\w\\w:\\w\\w:\\w\\w");
private boolean started = false;
private UUID serviceUuidAndAddress;
@@ -56,42 +61,60 @@ public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
// TODO: On some phones, this incorrectly returns false when the Bluetooth radio is off even though BLE advertise is supported
public static boolean isSupported(Context context) {
PackageManager packageManager = context.getPackageManager();
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || !packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH))
return false;
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
return bluetoothAdapter != null && getBluetoothAdapterAddress(bluetoothAdapter) != null && bluetoothAdapter.isMultipleAdvertisementSupported();
public enum CanStartResult {
CAN_START,
BLUETOOTH_OR_BLE_UNSUPPORTED,
BLUETOOTH_OFF,
BLUETOOTH_ADDRESS_UNAVAILABLE;
}
public static boolean isStartable(Context context) {
if (!isSupported(context))
return false;
public static CanStartResult canStart(Context context) {
PackageManager packageManager = context.getPackageManager();
BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(context);
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
return CanStartResult.BLUETOOTH_OFF;
} else if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) ||
!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) ||
!bluetoothAdapter.isMultipleAdvertisementSupported()) {
return CanStartResult.BLUETOOTH_OR_BLE_UNSUPPORTED;
} else if (getBluetoothAdapterAddress(bluetoothAdapter, context) == null) {
return CanStartResult.BLUETOOTH_ADDRESS_UNAVAILABLE;
}
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
return CanStartResult.CAN_START;
}
public static void startOrPromptBluetooth(Context context) {
if (!BluetoothSyncService.isSupported(context)) {
Log.d(TAG, "BLE not supported, not starting BLE sync service");
Toast.makeText(context, R.string.bluetooth_not_supported, Toast.LENGTH_LONG).show();
return;
switch (canStart(context)) {
case CAN_START:
Log.d(TAG, "Starting BLE sync service");
context.startService(new Intent(context, BluetoothSyncService.class));
break;
case BLUETOOTH_OR_BLE_UNSUPPORTED:
Log.d(TAG, "BLE not supported, not starting BLE sync service");
Toast.makeText(context, R.string.bluetooth_not_supported, Toast.LENGTH_LONG).show();
break;
case BLUETOOTH_OFF:
Log.d(TAG, "BLE supported but Bluetooth is off; will prompt for Bluetooth and start once it's on");
Toast.makeText(context, R.string.bluetooth_ask_enable, Toast.LENGTH_LONG).show();
context.startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE));
// BluetoothSyncServiceManager will start this service once Bluetooth is on.
break;
case BLUETOOTH_ADDRESS_UNAVAILABLE:
Log.d(TAG, "BLE supported but MAC address is unavailable; will prompt for address and start once it's available");
Toast.makeText(context, R.string.bluetooth_ask_address, Toast.LENGTH_LONG).show();
// TODO: Open the app's settings? Maybe getting the address should be part of onboarding UI
// BluetoothSyncServiceManager will start this (re)start this service when the address changes.
break;
}
}
if (BluetoothSyncService.isStartable(context)) {
Log.d(TAG, "BLE supported and Bluetooth is on; starting BLE sync service");
context.startService(new Intent(context, BluetoothSyncService.class));
} else {
Log.d(TAG, "BLE supported but Bluetooth is off; will prompt for Bluetooth and start once it's on");
Toast.makeText(context, R.string.bluetooth_ask, Toast.LENGTH_LONG).show();
context.startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE));
// BluetoothSyncServiceManager will start this service once Bluetooth is on.
}
private static BluetoothAdapter getBluetoothAdapter(Context context) {
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager == null)
return null;
return bluetoothManager.getAdapter();
}
private AdvertiseData buildAdvertiseData() {
@@ -127,7 +150,7 @@ private ScanSettings buildScanSettings() {
return builder.build();
}
private static String getBluetoothAdapterAddress(BluetoothAdapter bluetoothAdapter) {
private static String getBluetoothAdapterAddress(BluetoothAdapter bluetoothAdapter, Context context) {
@SuppressLint("HardwareIds") // Pair-free peer-to-peer communication should qualify as an "advanced telephony use case".
String address = bluetoothAdapter.getAddress();
@@ -163,8 +186,11 @@ private static String getBluetoothAdapterAddress(BluetoothAdapter bluetoothAdapt
// https://stackoverflow.com/a/35984808/702467
if (address.equals(FAKE_MAC_ADDRESS)) {
Log.w(TAG, "Android is actively blocking requests to get the MAC address");
return null;
// TODO: In this case, present UI that asks the user to manually copy the MAC address from settings
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
address = preferences.getString(SettingsActivity.KEY_BLUETOOTH_MAC, "").toUpperCase();
if (!MAC_PATTERN.matcher(address).matches())
return null;
}
return address;
@@ -208,6 +234,8 @@ public void onStartFailure(int errorCode) {
// Scan filters on service UUIDs were completely broken on the devices I tested (fully updated Google Pixel and Moto G4 Play as of March 2017)
// https://stackoverflow.com/questions/29664316/bluetooth-le-scan-filter-not-working
// TODO: Check if that's supported using bluetoothAdapter.isOffloadedFilteringSupported/isOffloadedScanBatchingSupported
// https://stackoverflow.com/questions/26482611/chipsets-devices-supporting-android-5-ble-peripheral-mode
bluetoothLeScanner.startScan(null /*filters*/, buildScanSettings(),
new ScanCallback() {
@Override
@@ -246,6 +274,9 @@ public void onScanResult(int callbackType, ScanResult result) {
}
private void stopBluetoothLeDiscovery() {
if (!bluetoothAdapter.isEnabled())
return;
if (bluetoothLeAdvertiser != null) {
bluetoothLeAdvertiser.stopAdvertising(new AdvertiseCallback() {
@Override
@@ -256,13 +287,15 @@ public void onStartFailure(int errorCode) {
});
}
bluetoothLeScanner.stopScan(new ScanCallback() {
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.e(TAG, "BLE scan failed to stop: error " + errorCode);
}
});
if (bluetoothLeScanner != null) {
bluetoothLeScanner.stopScan(new ScanCallback() {
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.e(TAG, "BLE scan failed to stop: error " + errorCode);
}
});
}
}
private class BluetoothClassicServer extends Thread {
@@ -280,7 +313,7 @@ public BluetoothClassicServer(UUID uuid) {
public void run() {
BluetoothSocket socket = null;
while (started) {
while (bluetoothAdapter.isEnabled() && started) {
String macAddress = null;
try {
// This will block until there is a connection
@@ -352,7 +385,7 @@ public void run() {
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
if (!isStartable(this)) {
if (canStart(this) != CanStartResult.CAN_START) {
Log.e(TAG, "Trying to start the service even though Bluetooth is off or BLE is unsupported");
stopSelf(startId);
return START_NOT_STICKY;
@@ -363,13 +396,12 @@ public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
bluetoothAdapter = getBluetoothAdapter(this);
// First half identifies that the advertisement is for Noise.
// Second half is the MAC address of this device's Bluetooth adapter so that clients know how to connect to it.
// These are not listed separately in the advertisement because a UUID is 16 bytes and ads are limited to 31 bytes.
String macAddress = getBluetoothAdapterAddress(bluetoothAdapter);
String macAddress = getBluetoothAdapterAddress(bluetoothAdapter, this);
if (macAddress == null) {
Log.e(TAG, "Unable to get this device's Bluetooth MAC address");
stopSelf(startId);
@@ -394,6 +426,8 @@ public int onStartCommand(Intent intent, int flags, int startId) {
@Override
public void onDestroy() {
started = false;
stopBluetoothLeDiscovery();
// TODO: Verify that this actually stops the thread
@@ -402,7 +436,6 @@ public void onDestroy() {
// TODO: Stop all BluetoothClassicClient threads
Toast.makeText(this, R.string.bluetooth_sync_stopped, Toast.LENGTH_LONG).show();
started = false;
Log.d(TAG, "Stopped");
super.onDestroy();
}
@@ -10,6 +10,8 @@
import com.alternativeinfrastructures.noise.R;
public class SettingsActivity extends AppCompatActivity {
public static final String KEY_BLUETOOTH_MAC = "pref_key_bluetooth_mac";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -5,8 +5,9 @@
<string name="action_raw_message_list">Debug: raw messages</string>
<string name="settings_title">Settings</string>
<string name="bluetooth_not_supported">Noise cannot sync over Bluetooth because Bluetooth LE is not supported on this device.</string>
<string name="bluetooth_ask">Enable Bluetooth to let Noise send and receive messages.</string>
<string name="bluetooth_not_supported">Noise cannot sync over Bluetooth because Bluetooth LE beacons are not supported on this device.</string>
<string name="bluetooth_ask_enable">Enable Bluetooth to let Noise send and receive messages.</string>
<string name="bluetooth_ask_address">Copy the Bluetooth hardware address into Noise\'s settings to send and receive messages.</string>
<string name="bluetooth_sync_started">Bluetooth is on. Noise will now send and receive messages in the background.</string>
<string name="bluetooth_sync_stopped">Bluetooth is off. Noise will not be able to send or receive messages.</string>

0 comments on commit 17d37c3

Please sign in to comment.