1717import android .bluetooth .le .ScanSettings ;
1818import android .content .Context ;
1919import android .content .Intent ;
20+ import android .content .SharedPreferences ;
2021import android .content .pm .PackageManager ;
2122import android .os .Build ;
2223import android .os .IBinder ;
2324import android .os .ParcelUuid ;
25+ import android .preference .PreferenceManager ;
2426import android .util .Log ;
2527import android .widget .Toast ;
2628
2729import net .vidageek .mirror .dsl .Mirror ;
2830
2931import com .alternativeinfrastructures .noise .R ;
3032import com .alternativeinfrastructures .noise .sync .StreamSync ;
33+ import com .alternativeinfrastructures .noise .views .SettingsActivity ;
3134
3235import java .io .IOException ;
3336import java .util .UUID ;
3437import java .util .concurrent .ConcurrentHashMap ;
38+ import java .util .regex .Pattern ;
3539
3640public class BluetoothSyncService extends Service {
3741 public static final String TAG = "BluetoothSyncService" ;
3842 public static final UUID SERVICE_UUID_HALF = UUID .fromString ("5ac825f4-6084-42a6-0000-000000000000" );
3943
4044 private static final String FAKE_MAC_ADDRESS = "02:00:00:00:00:00" ;
45+ private static final Pattern MAC_PATTERN = Pattern .compile ("\\ w\\ w:\\ w\\ w:\\ w\\ w:\\ w\\ w:\\ w\\ w:\\ w\\ w" );
4146
4247 private boolean started = false ;
4348 private UUID serviceUuidAndAddress ;
@@ -56,42 +61,60 @@ public IBinder onBind(Intent intent) {
5661 throw new UnsupportedOperationException ("Not yet implemented" );
5762 }
5863
59- // TODO: On some phones, this incorrectly returns false when the Bluetooth radio is off even though BLE advertise is supported
60- public static boolean isSupported (Context context ) {
61- PackageManager packageManager = context .getPackageManager ();
62- if (!packageManager .hasSystemFeature (PackageManager .FEATURE_BLUETOOTH_LE ) || !packageManager .hasSystemFeature (PackageManager .FEATURE_BLUETOOTH ))
63- return false ;
64-
65- BluetoothManager bluetoothManager = (BluetoothManager ) context .getSystemService (Context .BLUETOOTH_SERVICE );
66- BluetoothAdapter bluetoothAdapter = bluetoothManager .getAdapter ();
67- return bluetoothAdapter != null && getBluetoothAdapterAddress (bluetoothAdapter ) != null && bluetoothAdapter .isMultipleAdvertisementSupported ();
64+ public enum CanStartResult {
65+ CAN_START ,
66+ BLUETOOTH_OR_BLE_UNSUPPORTED ,
67+ BLUETOOTH_OFF ,
68+ BLUETOOTH_ADDRESS_UNAVAILABLE ;
6869 }
6970
70- public static boolean isStartable (Context context ) {
71- if (!isSupported (context ))
72- return false ;
71+ public static CanStartResult canStart (Context context ) {
72+ PackageManager packageManager = context .getPackageManager ();
73+ BluetoothAdapter bluetoothAdapter = getBluetoothAdapter (context );
74+ if (bluetoothAdapter == null || !bluetoothAdapter .isEnabled ()) {
75+ return CanStartResult .BLUETOOTH_OFF ;
76+ } else if (!packageManager .hasSystemFeature (PackageManager .FEATURE_BLUETOOTH_LE ) ||
77+ !packageManager .hasSystemFeature (PackageManager .FEATURE_BLUETOOTH ) ||
78+ !bluetoothAdapter .isMultipleAdvertisementSupported ()) {
79+ return CanStartResult .BLUETOOTH_OR_BLE_UNSUPPORTED ;
80+ } else if (getBluetoothAdapterAddress (bluetoothAdapter , context ) == null ) {
81+ return CanStartResult .BLUETOOTH_ADDRESS_UNAVAILABLE ;
82+ }
7383
74- BluetoothManager bluetoothManager = (BluetoothManager ) context .getSystemService (Context .BLUETOOTH_SERVICE );
75- BluetoothAdapter bluetoothAdapter = bluetoothManager .getAdapter ();
76- return bluetoothAdapter != null && bluetoothAdapter .isEnabled ();
84+ return CanStartResult .CAN_START ;
7785 }
7886
7987 public static void startOrPromptBluetooth (Context context ) {
80- if (!BluetoothSyncService .isSupported (context )) {
81- Log .d (TAG , "BLE not supported, not starting BLE sync service" );
82- Toast .makeText (context , R .string .bluetooth_not_supported , Toast .LENGTH_LONG ).show ();
83- return ;
88+ switch (canStart (context )) {
89+ case CAN_START :
90+ Log .d (TAG , "Starting BLE sync service" );
91+ context .startService (new Intent (context , BluetoothSyncService .class ));
92+ break ;
93+ case BLUETOOTH_OR_BLE_UNSUPPORTED :
94+ Log .d (TAG , "BLE not supported, not starting BLE sync service" );
95+ Toast .makeText (context , R .string .bluetooth_not_supported , Toast .LENGTH_LONG ).show ();
96+ break ;
97+ case BLUETOOTH_OFF :
98+ Log .d (TAG , "BLE supported but Bluetooth is off; will prompt for Bluetooth and start once it's on" );
99+ Toast .makeText (context , R .string .bluetooth_ask_enable , Toast .LENGTH_LONG ).show ();
100+ context .startActivity (new Intent (BluetoothAdapter .ACTION_REQUEST_ENABLE ));
101+ // BluetoothSyncServiceManager will start this service once Bluetooth is on.
102+ break ;
103+ case BLUETOOTH_ADDRESS_UNAVAILABLE :
104+ Log .d (TAG , "BLE supported but MAC address is unavailable; will prompt for address and start once it's available" );
105+ Toast .makeText (context , R .string .bluetooth_ask_address , Toast .LENGTH_LONG ).show ();
106+ // TODO: Open the app's settings? Maybe getting the address should be part of onboarding UI
107+ // BluetoothSyncServiceManager will start this (re)start this service when the address changes.
108+ break ;
84109 }
110+ }
85111
86- if (BluetoothSyncService .isStartable (context )) {
87- Log .d (TAG , "BLE supported and Bluetooth is on; starting BLE sync service" );
88- context .startService (new Intent (context , BluetoothSyncService .class ));
89- } else {
90- Log .d (TAG , "BLE supported but Bluetooth is off; will prompt for Bluetooth and start once it's on" );
91- Toast .makeText (context , R .string .bluetooth_ask , Toast .LENGTH_LONG ).show ();
92- context .startActivity (new Intent (BluetoothAdapter .ACTION_REQUEST_ENABLE ));
93- // BluetoothSyncServiceManager will start this service once Bluetooth is on.
94- }
112+ private static BluetoothAdapter getBluetoothAdapter (Context context ) {
113+ BluetoothManager bluetoothManager = (BluetoothManager ) context .getSystemService (Context .BLUETOOTH_SERVICE );
114+ if (bluetoothManager == null )
115+ return null ;
116+
117+ return bluetoothManager .getAdapter ();
95118 }
96119
97120 private AdvertiseData buildAdvertiseData () {
@@ -127,7 +150,7 @@ private ScanSettings buildScanSettings() {
127150 return builder .build ();
128151 }
129152
130- private static String getBluetoothAdapterAddress (BluetoothAdapter bluetoothAdapter ) {
153+ private static String getBluetoothAdapterAddress (BluetoothAdapter bluetoothAdapter , Context context ) {
131154 @ SuppressLint ("HardwareIds" ) // Pair-free peer-to-peer communication should qualify as an "advanced telephony use case".
132155 String address = bluetoothAdapter .getAddress ();
133156
@@ -163,8 +186,11 @@ private static String getBluetoothAdapterAddress(BluetoothAdapter bluetoothAdapt
163186 // https://stackoverflow.com/a/35984808/702467
164187 if (address .equals (FAKE_MAC_ADDRESS )) {
165188 Log .w (TAG , "Android is actively blocking requests to get the MAC address" );
166- return null ;
167- // TODO: In this case, present UI that asks the user to manually copy the MAC address from settings
189+
190+ SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences (context );
191+ address = preferences .getString (SettingsActivity .KEY_BLUETOOTH_MAC , "" ).toUpperCase ();
192+ if (!MAC_PATTERN .matcher (address ).matches ())
193+ return null ;
168194 }
169195
170196 return address ;
@@ -208,6 +234,8 @@ public void onStartFailure(int errorCode) {
208234
209235 // 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)
210236 // https://stackoverflow.com/questions/29664316/bluetooth-le-scan-filter-not-working
237+ // TODO: Check if that's supported using bluetoothAdapter.isOffloadedFilteringSupported/isOffloadedScanBatchingSupported
238+ // https://stackoverflow.com/questions/26482611/chipsets-devices-supporting-android-5-ble-peripheral-mode
211239 bluetoothLeScanner .startScan (null /*filters*/ , buildScanSettings (),
212240 new ScanCallback () {
213241 @ Override
@@ -246,6 +274,9 @@ public void onScanResult(int callbackType, ScanResult result) {
246274 }
247275
248276 private void stopBluetoothLeDiscovery () {
277+ if (!bluetoothAdapter .isEnabled ())
278+ return ;
279+
249280 if (bluetoothLeAdvertiser != null ) {
250281 bluetoothLeAdvertiser .stopAdvertising (new AdvertiseCallback () {
251282 @ Override
@@ -256,13 +287,15 @@ public void onStartFailure(int errorCode) {
256287 });
257288 }
258289
259- bluetoothLeScanner .stopScan (new ScanCallback () {
260- @ Override
261- public void onScanFailed (int errorCode ) {
262- super .onScanFailed (errorCode );
263- Log .e (TAG , "BLE scan failed to stop: error " + errorCode );
264- }
265- });
290+ if (bluetoothLeScanner != null ) {
291+ bluetoothLeScanner .stopScan (new ScanCallback () {
292+ @ Override
293+ public void onScanFailed (int errorCode ) {
294+ super .onScanFailed (errorCode );
295+ Log .e (TAG , "BLE scan failed to stop: error " + errorCode );
296+ }
297+ });
298+ }
266299 }
267300
268301 private class BluetoothClassicServer extends Thread {
@@ -280,7 +313,7 @@ public BluetoothClassicServer(UUID uuid) {
280313 public void run () {
281314 BluetoothSocket socket = null ;
282315
283- while (started ) {
316+ while (bluetoothAdapter . isEnabled () && started ) {
284317 String macAddress = null ;
285318 try {
286319 // This will block until there is a connection
@@ -352,7 +385,7 @@ public void run() {
352385 public int onStartCommand (Intent intent , int flags , int startId ) {
353386 super .onStartCommand (intent , flags , startId );
354387
355- if (! isStartable (this )) {
388+ if (canStart (this ) != CanStartResult . CAN_START ) {
356389 Log .e (TAG , "Trying to start the service even though Bluetooth is off or BLE is unsupported" );
357390 stopSelf (startId );
358391 return START_NOT_STICKY ;
@@ -363,13 +396,12 @@ public int onStartCommand(Intent intent, int flags, int startId) {
363396 return START_STICKY ;
364397 }
365398
366- BluetoothManager bluetoothManager = (BluetoothManager ) getSystemService (Context .BLUETOOTH_SERVICE );
367- bluetoothAdapter = bluetoothManager .getAdapter ();
399+ bluetoothAdapter = getBluetoothAdapter (this );
368400
369401 // First half identifies that the advertisement is for Noise.
370402 // Second half is the MAC address of this device's Bluetooth adapter so that clients know how to connect to it.
371403 // These are not listed separately in the advertisement because a UUID is 16 bytes and ads are limited to 31 bytes.
372- String macAddress = getBluetoothAdapterAddress (bluetoothAdapter );
404+ String macAddress = getBluetoothAdapterAddress (bluetoothAdapter , this );
373405 if (macAddress == null ) {
374406 Log .e (TAG , "Unable to get this device's Bluetooth MAC address" );
375407 stopSelf (startId );
@@ -394,6 +426,8 @@ public int onStartCommand(Intent intent, int flags, int startId) {
394426
395427 @ Override
396428 public void onDestroy () {
429+ started = false ;
430+
397431 stopBluetoothLeDiscovery ();
398432
399433 // TODO: Verify that this actually stops the thread
@@ -402,7 +436,6 @@ public void onDestroy() {
402436 // TODO: Stop all BluetoothClassicClient threads
403437
404438 Toast .makeText (this , R .string .bluetooth_sync_stopped , Toast .LENGTH_LONG ).show ();
405- started = false ;
406439 Log .d (TAG , "Stopped" );
407440 super .onDestroy ();
408441 }
0 commit comments