From a7576c060297b1048bec946fec00aa050794b6e0 Mon Sep 17 00:00:00 2001 From: Elias Lecomte Date: Tue, 13 Dec 2016 15:37:03 +0100 Subject: [PATCH] Feature/solve no le scanner bug (#16) * When Bluetooth is turned off, the BluetoothLeScanner is null. Change the DefaultBluetoothFactory to handle this scenario. * Don't run Travis-CI on their container based platform due to running out of memory exceptions. * Add jUnit tests. --- .travis.yml | 2 + .../DefaultBluetoothFactory.java | 44 ++++++++++-- .../ibeaconscanner/DefaultScanService.java | 27 +++---- .../interfaces/BluetoothFactory.java | 9 ++- .../DefaultScanServiceTest.java | 71 +++++++++++++++++++ .../ibeaconscanner/InitializationTest.java | 33 +-------- .../beacons/ibeaconscanner/StartScanTest.java | 67 +++++++++++++++++ 7 files changed, 194 insertions(+), 59 deletions(-) create mode 100644 ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanServiceTest.java create mode 100644 ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/StartScanTest.java diff --git a/.travis.yml b/.travis.yml index db21828..32873f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: android +sudo: required + android: components: - platform-tools diff --git a/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultBluetoothFactory.java b/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultBluetoothFactory.java index 5ca3738..7bc5e71 100644 --- a/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultBluetoothFactory.java +++ b/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultBluetoothFactory.java @@ -15,20 +15,42 @@ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class DefaultBluetoothFactory implements BluetoothFactory { - private BluetoothLeScanner bluetoothLeScanner; + private BluetoothAdapter bluetoothAdapter; /** - * Creates the {@link #bluetoothLeScanner} if it is null. Throws a {@link SecurityException} when the bluetooth permission is not granted. + * Attaches the {@link #bluetoothAdapter} if it is null. + * + * @return true if the {@link BluetoothAdapter} and {@link BluetoothLeScanner} are available */ @Override - public void createBluetoothLeScanner() + public boolean canAttachBluetoothAdapter() { - if (this.bluetoothLeScanner == null) + // try to get the BluetoothAdapter + // apps running in a Samsung Knox container will crash with a SecurityException + if (this.bluetoothAdapter == null) { + try + { + this.bluetoothAdapter = this.getBluetoothAdapter(); + } + catch (final SecurityException securityException) + { + return false; + } + } - final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - this.bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); + // try to get the BluetoothLeScanner + // apps without Bluetooth permission will crash with a SecurityException + try + { + this.bluetoothAdapter.getBluetoothLeScanner(); } + catch (final SecurityException securityException) + { + return false; + } + + return true; } /** @@ -38,6 +60,14 @@ public void createBluetoothLeScanner() @Nullable public BluetoothLeScanner getBluetoothLeScanner() { - return this.bluetoothLeScanner; + return this.canAttachBluetoothAdapter() ? this.bluetoothAdapter.getBluetoothLeScanner() : null; + } + + /** + * @return {@link BluetoothAdapter#getDefaultAdapter()} + */ + public BluetoothAdapter getBluetoothAdapter() + { + return BluetoothAdapter.getDefaultAdapter(); } } diff --git a/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanService.java b/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanService.java index e83f6b0..40bb5d0 100644 --- a/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanService.java +++ b/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanService.java @@ -129,26 +129,17 @@ public void timedOut(final Object anObject) // check if we can scan boolean canScan = true; - // may have to reattach the BluetoothLeScanner - // verify if we can get the BluetoothAdapter (Samsung Knox disables Bluetooth permission). - if (this.bluetoothFactory.getBluetoothLeScanner() == null) + // reattach the BluetoothAdapter + if (!this.bluetoothFactory.canAttachBluetoothAdapter()) { - try - { - this.bluetoothFactory.createBluetoothLeScanner(); - } - catch (final SecurityException securityException) - { - canScan = false; + canScan = false; - if (this.callback != null) - { - this.callback.monitoringDidFail(Error.NO_BLUETOOTH_PERMISSION); - } + if (this.callback != null) + { + this.callback.monitoringDidFail(Error.NO_BLUETOOTH_PERMISSION); } } - - if (this.bluetoothFactory.getBluetoothLeScanner() != null) + else { if (!BluetoothUtils.hasBluetoothLE(this.context)) { @@ -191,7 +182,7 @@ public void timedOut(final Object anObject) } } - if (canScan) + if (canScan && this.bluetoothFactory.getBluetoothLeScanner() != null) { // stop scanning this.bluetoothFactory.getBluetoothLeScanner().stopScan(DefaultScanService.this.scannerScanCallback); @@ -286,7 +277,7 @@ public Initializer setAddBeaconTimeoutInMillis(final long addBeaconTimeoutInMill } /** - * Additionaly you can set a {@link BluetoothFactory} responsible for creating a {@link android.bluetooth.le.BluetoothLeScanner}. + * Additionally you can set a {@link BluetoothFactory} responsible for creating a {@link android.bluetooth.le.BluetoothLeScanner}. * * @param bluetoothFactory to use * @return {@link DefaultScanService.Initializer} diff --git a/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/interfaces/BluetoothFactory.java b/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/interfaces/BluetoothFactory.java index 073776b..3d1b204 100644 --- a/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/interfaces/BluetoothFactory.java +++ b/ibeaconscanner/src/main/java/mobi/inthepocket/android/beacons/ibeaconscanner/interfaces/BluetoothFactory.java @@ -1,8 +1,10 @@ package mobi.inthepocket.android.beacons.ibeaconscanner.interfaces; import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.BluetoothLeScanner; import android.os.Build; +import android.support.annotation.Nullable; /** * BluetoothFactory is responsible for managing the {@link BluetoothLeScanner} used. @@ -12,12 +14,15 @@ public interface BluetoothFactory { /** - * Create a {@link BluetoothLeScanner}. Throws a {@link SecurityException} when the bluetooth permission is not granted. + * Attaches the {@link BluetoothAdapter} if it is null. + * + * @return true if the {@link BluetoothAdapter} and {@link BluetoothLeScanner} are available */ - void createBluetoothLeScanner(); + boolean canAttachBluetoothAdapter(); /** * @return a {@link BluetoothLeScanner} */ + @Nullable BluetoothLeScanner getBluetoothLeScanner(); } diff --git a/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanServiceTest.java b/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanServiceTest.java new file mode 100644 index 0000000..fe9113f --- /dev/null +++ b/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/DefaultScanServiceTest.java @@ -0,0 +1,71 @@ +package mobi.inthepocket.android.beacons.ibeaconscanner; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.os.Build; + +import junit.framework.Assert; + +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by eliaslecomte on 13/12/2016. + */ + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class DefaultScanServiceTest +{ + @Test + public void testAppRunningInSamsungKnoxContainer() + { + final DefaultBluetoothFactory defaultBluetoothFactory = mock(DefaultBluetoothFactory.class); + Mockito.doThrow(new SecurityException("Need BLUETOOTH permission: Neither user xxxxx nor current process has android.permission.BLUETOOTH")) + .when(defaultBluetoothFactory).getBluetoothAdapter(); + when(defaultBluetoothFactory.canAttachBluetoothAdapter()).thenCallRealMethod(); + when(defaultBluetoothFactory.getBluetoothLeScanner()).thenCallRealMethod(); + + Assert.assertEquals(false, defaultBluetoothFactory.canAttachBluetoothAdapter()); + Assert.assertEquals(null, defaultBluetoothFactory.getBluetoothLeScanner()); + } + + @Test + public void testSamsungKnox() + { + + } + + @Test + public void testMissingBluetoothPermission() + { + final BluetoothAdapter bluetoothAdapter = mock(BluetoothAdapter.class); + Mockito.doThrow(new SecurityException("Need BLUETOOTH permission: Neither user xxxxx nor current process has android.permission.BLUETOOTH")) + .when(bluetoothAdapter).getBluetoothLeScanner(); + + final DefaultBluetoothFactory defaultBluetoothFactory = mock(DefaultBluetoothFactory.class); + when(defaultBluetoothFactory.canAttachBluetoothAdapter()).thenCallRealMethod(); + when(defaultBluetoothFactory.getBluetoothLeScanner()).thenCallRealMethod(); + when(defaultBluetoothFactory.getBluetoothAdapter()).thenReturn(bluetoothAdapter); + + Assert.assertEquals(false, defaultBluetoothFactory.canAttachBluetoothAdapter()); + Assert.assertEquals(null, defaultBluetoothFactory.getBluetoothLeScanner()); + } + + @Test + public void testBluetoothOff() + { + final BluetoothAdapter bluetoothAdapter = mock(BluetoothAdapter.class); + when(bluetoothAdapter.getBluetoothLeScanner()).thenReturn(null); + + final DefaultBluetoothFactory defaultBluetoothFactory = mock(DefaultBluetoothFactory.class); + when(defaultBluetoothFactory.canAttachBluetoothAdapter()).thenCallRealMethod(); + when(defaultBluetoothFactory.getBluetoothLeScanner()).thenCallRealMethod(); + when(defaultBluetoothFactory.getBluetoothAdapter()).thenReturn(bluetoothAdapter); + + Assert.assertEquals(true, defaultBluetoothFactory.canAttachBluetoothAdapter()); + Assert.assertEquals(null, defaultBluetoothFactory.getBluetoothLeScanner()); + } +} diff --git a/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/InitializationTest.java b/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/InitializationTest.java index 848a21b..9324a35 100644 --- a/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/InitializationTest.java +++ b/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/InitializationTest.java @@ -4,17 +4,10 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import mobi.inthepocket.android.beacons.ibeaconscanner.interfaces.BluetoothFactory; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - /** * Created by eliaslecomte on 12/12/2016. */ @@ -24,32 +17,8 @@ public class InitializationTest { @Test - public void init() + public void regularInitialize() { IBeaconScanner.initialize(IBeaconScanner.newInitializer(RuntimeEnvironment.application).build()); } - - @Test - public void initWithSamsungKnox() - { - // apps running in a Samsung Knox container do not have Bluetooth permission. - - // create a BluetoothFactory that always throws the SecurityException like when on Samsung Knox - final BluetoothFactory bluetoothFactory = mock(BluetoothFactory.class); - Mockito.doThrow(new SecurityException("Need BLUETOOTH permission: Neither user xxxxx nor current process has android.permission.BLUETOOTH")) - .when(bluetoothFactory).createBluetoothLeScanner(); - - // create a ScanService with the BluetoothFactory set - final DefaultScanService scanService = IBeaconScanner.newInitializer(RuntimeEnvironment.application) - .setBluetoothFactory(bluetoothFactory) - .build(); - - IBeaconScanner.initialize(scanService); - - // test start scanning logic - scanService.timedOut(new Object()); - - verify(bluetoothFactory, times(1)).createBluetoothLeScanner(); - verify(bluetoothFactory, times(2)).getBluetoothLeScanner(); - } } diff --git a/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/StartScanTest.java b/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/StartScanTest.java new file mode 100644 index 0000000..a56f8ea --- /dev/null +++ b/ibeaconscanner/src/test/java/mobi/inthepocket/android/beacons/ibeaconscanner/StartScanTest.java @@ -0,0 +1,67 @@ +package mobi.inthepocket.android.beacons.ibeaconscanner; + +import android.os.Build; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import mobi.inthepocket.android.beacons.ibeaconscanner.interfaces.BluetoothFactory; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Created by eliaslecomte on 13/12/2016. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.LOLLIPOP) +public class StartScanTest +{ + @Test + public void startWithBluetoothOff() + { + final BluetoothFactory bluetoothFactory = mock(BluetoothFactory.class); + when(bluetoothFactory.canAttachBluetoothAdapter()).thenReturn(true); + when(bluetoothFactory.getBluetoothLeScanner()).thenReturn(null); + + // create a ScanService with the BluetoothFactory set + final DefaultScanService scanService = IBeaconScanner.newInitializer(RuntimeEnvironment.application) + .setBluetoothFactory(bluetoothFactory) + .build(); + + IBeaconScanner.initialize(scanService); + + // test start scanning + scanService.timedOut(new Object()); + + verify(bluetoothFactory, times(1)).canAttachBluetoothAdapter(); + verify(bluetoothFactory, times(0)).getBluetoothLeScanner(); + } + + @Test + public void startWithMissingPermissions() + { + final BluetoothFactory bluetoothFactory = mock(BluetoothFactory.class); + when(bluetoothFactory.canAttachBluetoothAdapter()).thenReturn(false); + when(bluetoothFactory.getBluetoothLeScanner()).thenReturn(null); + + // create a ScanService with the BluetoothFactory set + final DefaultScanService scanService = IBeaconScanner.newInitializer(RuntimeEnvironment.application) + .setBluetoothFactory(bluetoothFactory) + .build(); + + IBeaconScanner.initialize(scanService); + + // test start scanning + scanService.timedOut(new Object()); + + verify(bluetoothFactory, times(1)).canAttachBluetoothAdapter(); + verify(bluetoothFactory, times(0)).getBluetoothLeScanner(); + } +}