From 0f0ab7e0148a6a53d0258edac025a621bf1de5a1 Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 11:09:06 -0800 Subject: [PATCH 01/28] add basic sense service --- .../commonsense/service/SenseService.java | 213 ++++++++++++++++++ .../service/SenseServiceHelper.java | 126 +++++++++++ 2 files changed, 339 insertions(+) create mode 100644 ble/src/main/java/is/hello/commonsense/service/SenseService.java create mode 100644 ble/src/main/java/is/hello/commonsense/service/SenseServiceHelper.java diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java new file mode 100644 index 0000000..a27e173 --- /dev/null +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -0,0 +1,213 @@ +package is.hello.commonsense.service; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.support.annotation.CheckResult; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.List; + +import is.hello.buruberi.bluetooth.errors.ConnectionStateException; +import is.hello.buruberi.bluetooth.stacks.GattPeripheral; +import is.hello.buruberi.bluetooth.stacks.util.AdvertisingData; +import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; +import is.hello.commonsense.bluetooth.SenseIdentifiers; +import is.hello.commonsense.bluetooth.SensePeripheral; +import is.hello.commonsense.bluetooth.model.SenseConnectToWiFiUpdate; +import is.hello.commonsense.bluetooth.model.SenseLedAnimation; +import is.hello.commonsense.bluetooth.model.SenseNetworkStatus; +import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_endpoint; +import is.hello.commonsense.util.ConnectProgress; +import is.hello.commonsense.util.Functions; +import rx.Observable; +import rx.functions.Action0; + +public class SenseService extends Service { + private @Nullable SensePeripheral sense; + + //region Service Lifecycle + + private final LocalBinder binder = new LocalBinder(); + + class LocalBinder extends Binder { + @NonNull + SenseService getService() { + return SenseService.this; + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + //endregion + + + //region Utilities + + private static Exception createNoDeviceException() { + return new ConnectionStateException("Not connected to Sense"); + } + + //endregion + + + //region Managing Connectivity + + public void preparePeripheralCriteria(@NonNull PeripheralCriteria criteria, + @Nullable String deviceId) { + criteria.addExactMatchPredicate(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT); + if (deviceId != null) { + criteria.setLimit(1); + criteria.addStartsWithPredicate(AdvertisingData.TYPE_SERVICE_DATA, + SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + deviceId); + } + } + + @CheckResult + public Observable connect(@NonNull GattPeripheral peripheral) { + if (this.sense != null) { + return Observable.error(new IllegalStateException("Cannot connect to multiple Senses at once.")); + } + + this.sense = new SensePeripheral(peripheral); + if (sense.isConnected()) { + return Observable.just(ConnectProgress.CONNECTED); + } + + return sense.connect(); + } + + @CheckResult + public Observable disconnect() { + if (sense == null) { + return Observable.just(null); + } + + return sense.disconnect() + .map(Functions.createMapperToVoid()) + .doOnTerminate(new Action0() { + @CheckResult + @Override + public void call() { + SenseService.this.sense = null; + } + }); + } + + @CheckResult + public Observable removeBond() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.removeBond() + .map(Functions.createMapperToVoid()); + } + + //endregion + + + //region Commands + + @CheckResult + public Observable runLedAnimation(@NonNull SenseLedAnimation animationType) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.runLedAnimation(animationType); + } + + @CheckResult + public Observable> scanForWifiNetworks(@Nullable SensePeripheral.CountryCode countryCode) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.scanForWifiNetworks(countryCode); + } + + @CheckResult + public Observable currentWifiNetwork() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.getWifiNetwork(); + } + + @CheckResult + public Observable sendWifiCredentials(@NonNull String ssid, + @NonNull wifi_endpoint.sec_type securityType, + @NonNull String password) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.connectToWiFiNetwork(ssid, securityType, password); + } + + @CheckResult + public Observable linkAccount(@NonNull String accessToken) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.linkAccount(accessToken); + } + + @CheckResult + public Observable linkPill(@NonNull String accessToken) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.pairPill(accessToken); + } + + @CheckResult + public Observable pushData() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.pushData(); + } + + @CheckResult + public Observable putIntoPairingMode() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.putIntoPairingMode(); + } + + @CheckResult + public Observable factoryReset() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.factoryReset(); + } + + //endregion +} diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseServiceHelper.java b/ble/src/main/java/is/hello/commonsense/service/SenseServiceHelper.java new file mode 100644 index 0000000..97595b4 --- /dev/null +++ b/ble/src/main/java/is/hello/commonsense/service/SenseServiceHelper.java @@ -0,0 +1,126 @@ +package is.hello.commonsense.service; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to facilitate communication between a component and the {@link SenseService}. + */ +public class SenseServiceHelper implements ServiceConnection { + private final Context context; + private final List consumers = new ArrayList<>(); + private @Nullable SenseService senseService; + + //region Lifecycle + + /** + * Construct a service helper. + * @param context The context whose lifecycle this helper will be bound to. + */ + public SenseServiceHelper(@NonNull Context context) { + this.context = context; + } + + /** + * Binds the service helper to the {@link SenseService}. + */ + public void create() { + final Intent intent = new Intent(context, SenseService.class); + context.bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + /** + * Unbinds the service helper from the @{link SenseService}. + */ + public void destroy() { + context.unbindService(this); + } + + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + this.senseService = ((SenseService.LocalBinder) service).getService(); + + for (int i = consumers.size() - 1; i >= 0; i--) { + consumers.get(i).onSenseServiceAvailable(senseService); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + this.senseService = null; + + for (int i = consumers.size() - 1; i >= 0; i--) { + consumers.get(i).onSenseServiceUnavailable(); + } + } + + //endregion + + + //region Consumers + + /** + * Register a new consumer with the helper. The consumer will receive an immediate + * callback if the service helper is already bound to the service. + * @param consumer The consumer. + */ + public void registerConsumer(@NonNull Consumer consumer) { + consumers.add(consumer); + + if (senseService != null) { + consumer.onSenseServiceAvailable(senseService); + } + } + + /** + * Unregister a consumer from the helper. Safe to call within {@link Consumer} callbacks. + * @param consumer The consumer. + */ + public void unregisterConsumer(@NonNull Consumer consumer) { + consumers.remove(consumer); + } + + /** + * Unregisters all consumers from the helper. Not safe to call within {@link Consumer} callbacks. + */ + public void removeAllConsumers() { + consumers.clear(); + } + + /** + * @return The {@link SenseService} if it's currently bound; {@code null} otherwise. + */ + @Nullable + public SenseService getSenseService() { + return senseService; + } + + //endregion + + + /** + * Specifies a class that is interested in communicating with the {@link SenseService}. + */ + public interface Consumer { + /** + * Called when the {@link SenseService} is available for usel + * @param service The service. + */ + void onSenseServiceAvailable(@NonNull SenseService service); + + /** + * Called when the {@link SenseService} becomes unavailable. Any external + * references to it should be immediately cleared. + */ + void onSenseServiceUnavailable(); + } +} From 09998d1ae4558dd2dcea2ee85a99dd57529ffc0e Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 11:12:32 -0800 Subject: [PATCH 02/28] fix incorrectly named test cases --- ...alTests.java => SensePeripheralTests.java} | 2 +- ....java => ProtobufPacketListenerTests.java} | 34 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) rename ble/src/test/java/is/hello/commonsense/bluetooth/{CommonSensePeripheralTests.java => SensePeripheralTests.java} (99%) rename ble/src/test/java/is/hello/commonsense/bluetooth/model/{CommonSensePacketHandlerTests.java => ProtobufPacketListenerTests.java} (75%) diff --git a/ble/src/test/java/is/hello/commonsense/bluetooth/CommonSensePeripheralTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java similarity index 99% rename from ble/src/test/java/is/hello/commonsense/bluetooth/CommonSensePeripheralTests.java rename to ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java index 8452bf7..2a3607c 100644 --- a/ble/src/test/java/is/hello/commonsense/bluetooth/CommonSensePeripheralTests.java +++ b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java @@ -45,7 +45,7 @@ import static org.mockito.Mockito.verify; @SuppressWarnings("ResourceType") -public class CommonSensePeripheralTests extends CommonSenseTestCase { +public class SensePeripheralTests extends CommonSenseTestCase { private static final String TEST_DEVICE_ID = "CA154FFA"; //region Vending Mocks diff --git a/ble/src/test/java/is/hello/commonsense/bluetooth/model/CommonSensePacketHandlerTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java similarity index 75% rename from ble/src/test/java/is/hello/commonsense/bluetooth/model/CommonSensePacketHandlerTests.java rename to ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java index d1e64c7..0e60fa5 100644 --- a/ble/src/test/java/is/hello/commonsense/bluetooth/model/CommonSensePacketHandlerTests.java +++ b/ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java @@ -17,8 +17,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -public class CommonSensePacketHandlerTests extends CommonSenseTestCase { - private final ProtobufPacketListener packetHandler = new ProtobufPacketListener(); +public class ProtobufPacketListenerTests extends CommonSenseTestCase { + private final ProtobufPacketListener packetListener = new ProtobufPacketListener(); private static final byte[] LONG_SEQUENCE = { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x07, 0x08, 0x09, @@ -31,7 +31,7 @@ public class CommonSensePacketHandlerTests extends CommonSenseTestCase { @Test public void createPackets() throws Exception { - List packets = packetHandler.createOutgoingPackets(LONG_SEQUENCE); + List packets = packetListener.createOutgoingPackets(LONG_SEQUENCE); assertEquals(4, packets.size()); int index = 0; for (byte[] packet : packets) { @@ -42,13 +42,13 @@ public void createPackets() throws Exception { @Test public void allPacketsRightLength() throws Exception { byte[] failureCase = Bytes.fromString("08011002320832574952453137373A083257495245313737420A303132333435363738397803"); - List failureCasePackets = packetHandler.createOutgoingPackets(failureCase); + List failureCasePackets = packetListener.createOutgoingPackets(failureCase); for (byte[] packet : failureCasePackets) { assertTrue(packet.length <= 20); } byte[] successCase = Bytes.fromString("080110023A083257495245313737420A303132333435363738397803"); - List successCasePackets = packetHandler.createOutgoingPackets(successCase); + List successCasePackets = packetListener.createOutgoingPackets(successCase); for (byte[] packet : successCasePackets) { assertTrue(packet.length <= 20); } @@ -56,16 +56,16 @@ public void allPacketsRightLength() throws Exception { @Test public void shouldProcessCharacteristic() throws Exception { - assertTrue(packetHandler.parser.canProcessPacket(SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE)); - assertFalse(packetHandler.parser.canProcessPacket(UUID.fromString("D1700CFA-A6F8-47FC-92F5-9905D15F261C"))); + assertTrue(packetListener.parser.canProcessPacket(SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE)); + assertFalse(packetListener.parser.canProcessPacket(UUID.fromString("D1700CFA-A6F8-47FC-92F5-9905D15F261C"))); } @Test public void processPacketOutOfOrder() throws Exception { TestResponseListener responseListener = new TestResponseListener(); - packetHandler.setResponseListener(responseListener); + packetListener.setResponseListener(responseListener); - packetHandler.parser.processPacket(new byte[]{99}); + packetListener.parser.processPacket(new byte[]{99}); assertNull(responseListener.data); assertNotNull(responseListener.error); @@ -76,20 +76,20 @@ public void stateCleanUp() throws Exception { byte[] testPacket = { 0, /* packetCount */ 2, 0x00 }; TestResponseListener responseListener = new TestResponseListener(); - packetHandler.setResponseListener(responseListener); + packetListener.setResponseListener(responseListener); - packetHandler.parser.processPacket(testPacket); + packetListener.parser.processPacket(testPacket); assertNull(responseListener.data); assertNull(responseListener.error); - packetHandler.onPeripheralDisconnected(); + packetListener.onPeripheralDisconnected(); responseListener.reset(); - packetHandler.setResponseListener(responseListener); + packetListener.setResponseListener(responseListener); - packetHandler.parser.processPacket(testPacket); + packetListener.parser.processPacket(testPacket); assertNull(responseListener.data); assertNull(responseListener.error); @@ -104,13 +104,13 @@ public void processPacketInOrder() throws Exception { .setVersion(0) .build(); - List rawPackets = packetHandler.createOutgoingPackets(morpheusCommand.toByteArray()); + List rawPackets = packetListener.createOutgoingPackets(morpheusCommand.toByteArray()); TestResponseListener responseListener = new TestResponseListener(); - packetHandler.setResponseListener(responseListener); + packetListener.setResponseListener(responseListener); for (byte[] packet : rawPackets) { - packetHandler.parser.processPacket(packet); + packetListener.parser.processPacket(packet); } assertNull(responseListener.error); From 1928a95cfcc3af0dd5b31bb35290d61d1fab6f0f Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 11:34:54 -0800 Subject: [PATCH 03/28] move common sense test case class --- .../is/hello/commonsense/{util => }/CommonSenseTestCase.java | 4 +--- .../is/hello/commonsense/bluetooth/SensePeripheralTests.java | 2 +- .../bluetooth/model/ProtobufPacketListenerTests.java | 2 +- .../java/is/hello/commonsense/util/CompatibilityTests.java | 2 ++ ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java | 2 +- .../test/java/is/hello/commonsense/util/StringRefTests.java | 1 + 6 files changed, 7 insertions(+), 6 deletions(-) rename ble/src/test/java/is/hello/commonsense/{util => }/CommonSenseTestCase.java (85%) diff --git a/ble/src/test/java/is/hello/commonsense/util/CommonSenseTestCase.java b/ble/src/test/java/is/hello/commonsense/CommonSenseTestCase.java similarity index 85% rename from ble/src/test/java/is/hello/commonsense/util/CommonSenseTestCase.java rename to ble/src/test/java/is/hello/commonsense/CommonSenseTestCase.java index fe178d1..924d111 100644 --- a/ble/src/test/java/is/hello/commonsense/util/CommonSenseTestCase.java +++ b/ble/src/test/java/is/hello/commonsense/CommonSenseTestCase.java @@ -1,4 +1,4 @@ -package is.hello.commonsense.util; +package is.hello.commonsense; import android.content.Context; @@ -7,8 +7,6 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import is.hello.commonsense.BuildConfig; - @RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) diff --git a/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java index 2a3607c..29808aa 100644 --- a/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java +++ b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java @@ -23,8 +23,8 @@ import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; import is.hello.buruberi.util.AdvertisingDataBuilder; import is.hello.buruberi.util.Operation; +import is.hello.commonsense.CommonSenseTestCase; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos; -import is.hello.commonsense.util.CommonSenseTestCase; import is.hello.commonsense.util.Sync; import rx.Observable; import rx.schedulers.Schedulers; diff --git a/ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java index 0e60fa5..88f3420 100644 --- a/ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java +++ b/ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java @@ -6,10 +6,10 @@ import java.util.UUID; import is.hello.buruberi.bluetooth.stacks.util.Bytes; +import is.hello.commonsense.CommonSenseTestCase; import is.hello.commonsense.bluetooth.SenseIdentifiers; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.MorpheusCommand; -import is.hello.commonsense.util.CommonSenseTestCase; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/ble/src/test/java/is/hello/commonsense/util/CompatibilityTests.java b/ble/src/test/java/is/hello/commonsense/util/CompatibilityTests.java index fa39509..a055cbc 100644 --- a/ble/src/test/java/is/hello/commonsense/util/CompatibilityTests.java +++ b/ble/src/test/java/is/hello/commonsense/util/CompatibilityTests.java @@ -2,6 +2,8 @@ import org.junit.Test; +import is.hello.commonsense.CommonSenseTestCase; + import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; diff --git a/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java b/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java index d116bcc..f0b981d 100644 --- a/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java +++ b/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java @@ -8,6 +8,7 @@ import is.hello.buruberi.bluetooth.errors.BondException; import is.hello.buruberi.bluetooth.errors.BuruberiException; +import is.hello.commonsense.CommonSenseTestCase; import is.hello.commonsense.R; import static junit.framework.Assert.assertEquals; @@ -16,7 +17,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; public class ErrorsTests extends CommonSenseTestCase { diff --git a/ble/src/test/java/is/hello/commonsense/util/StringRefTests.java b/ble/src/test/java/is/hello/commonsense/util/StringRefTests.java index c486d5b..751e9b1 100644 --- a/ble/src/test/java/is/hello/commonsense/util/StringRefTests.java +++ b/ble/src/test/java/is/hello/commonsense/util/StringRefTests.java @@ -7,6 +7,7 @@ import org.junit.Test; +import is.hello.commonsense.CommonSenseTestCase; import is.hello.commonsense.R; import static junit.framework.Assert.assertEquals; From 6f3e18178ef29894d5c27c06de120bef334f9e2d Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 11:35:12 -0800 Subject: [PATCH 04/28] rename sense service helper; add test case for connection lifecycle --- ...elper.java => SenseServiceConnection.java} | 42 ++++++------ .../service/SenseServiceTests.java | 66 +++++++++++++++++++ 2 files changed, 87 insertions(+), 21 deletions(-) rename ble/src/main/java/is/hello/commonsense/service/{SenseServiceHelper.java => SenseServiceConnection.java} (71%) create mode 100644 ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseServiceHelper.java b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java similarity index 71% rename from ble/src/main/java/is/hello/commonsense/service/SenseServiceHelper.java rename to ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java index 97595b4..67cce1d 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseServiceHelper.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java @@ -14,9 +14,9 @@ /** * Helper class to facilitate communication between a component and the {@link SenseService}. */ -public class SenseServiceHelper implements ServiceConnection { +public class SenseServiceConnection implements ServiceConnection { private final Context context; - private final List consumers = new ArrayList<>(); + private final List listeners = new ArrayList<>(); private @Nullable SenseService senseService; //region Lifecycle @@ -25,7 +25,7 @@ public class SenseServiceHelper implements ServiceConnection { * Construct a service helper. * @param context The context whose lifecycle this helper will be bound to. */ - public SenseServiceHelper(@NonNull Context context) { + public SenseServiceConnection(@NonNull Context context) { this.context = context; } @@ -49,8 +49,8 @@ public void destroy() { public void onServiceConnected(ComponentName name, IBinder service) { this.senseService = ((SenseService.LocalBinder) service).getService(); - for (int i = consumers.size() - 1; i >= 0; i--) { - consumers.get(i).onSenseServiceAvailable(senseService); + for (int i = listeners.size() - 1; i >= 0; i--) { + listeners.get(i).onSenseServiceConnected(senseService); } } @@ -58,8 +58,8 @@ public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceDisconnected(ComponentName name) { this.senseService = null; - for (int i = consumers.size() - 1; i >= 0; i--) { - consumers.get(i).onSenseServiceUnavailable(); + for (int i = listeners.size() - 1; i >= 0; i--) { + listeners.get(i).onSenseServiceDisconnected(); } } @@ -71,29 +71,29 @@ public void onServiceDisconnected(ComponentName name) { /** * Register a new consumer with the helper. The consumer will receive an immediate * callback if the service helper is already bound to the service. - * @param consumer The consumer. + * @param listener The consumer. */ - public void registerConsumer(@NonNull Consumer consumer) { - consumers.add(consumer); + public void registerConsumer(@NonNull Listener listener) { + listeners.add(listener); if (senseService != null) { - consumer.onSenseServiceAvailable(senseService); + listener.onSenseServiceConnected(senseService); } } /** - * Unregister a consumer from the helper. Safe to call within {@link Consumer} callbacks. - * @param consumer The consumer. + * Unregister a consumer from the helper. Safe to call within {@link Listener} callbacks. + * @param listener The consumer. */ - public void unregisterConsumer(@NonNull Consumer consumer) { - consumers.remove(consumer); + public void unregisterConsumer(@NonNull Listener listener) { + listeners.remove(listener); } /** - * Unregisters all consumers from the helper. Not safe to call within {@link Consumer} callbacks. + * Unregisters all consumers from the helper. Not safe to call within {@link Listener} callbacks. */ public void removeAllConsumers() { - consumers.clear(); + listeners.clear(); } /** @@ -110,17 +110,17 @@ public SenseService getSenseService() { /** * Specifies a class that is interested in communicating with the {@link SenseService}. */ - public interface Consumer { + public interface Listener { /** - * Called when the {@link SenseService} is available for usel + * Called when the {@link SenseService} is available for use. * @param service The service. */ - void onSenseServiceAvailable(@NonNull SenseService service); + void onSenseServiceConnected(@NonNull SenseService service); /** * Called when the {@link SenseService} becomes unavailable. Any external * references to it should be immediately cleared. */ - void onSenseServiceUnavailable(); + void onSenseServiceDisconnected(); } } diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java new file mode 100644 index 0000000..f724d87 --- /dev/null +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -0,0 +1,66 @@ +package is.hello.commonsense.service; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.robolectric.shadows.ShadowApplication; + +import is.hello.commonsense.CommonSenseTestCase; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SenseServiceTests extends CommonSenseTestCase { + private SenseService service; + + //region Lifecycle + + @Before + public void setUp() throws Exception { + final Context context = getContext(); + final ComponentName componentName = new ComponentName(context, SenseService.class); + this.service = new SenseService(); + service.onCreate(); + + final IBinder binder = service.onBind(new Intent(context, SenseService.class)); + ShadowApplication.getInstance().setComponentNameAndServiceForBindService(componentName, + binder); + } + + @After + public void tearDown() throws Exception { + service.onDestroy(); + } + + //endregion + + + static SenseServiceConnection.Listener createMockConsumer() { + return mock(SenseServiceConnection.Listener.class); + } + + @Test + public void connection() { + final SenseServiceConnection.Listener listener = createMockConsumer(); + final SenseServiceConnection helper = new SenseServiceConnection(getContext()); + helper.registerConsumer(listener); + helper.create(); + + assertThat(helper.getSenseService(), is(notNullValue())); + //noinspection ConstantConditions + verify(listener).onSenseServiceConnected(helper.getSenseService()); + + helper.destroy(); + assertThat(helper.getSenseService(), is(nullValue())); + verify(listener).onSenseServiceDisconnected(); + } +} From e64c8d50c0bce4832133ec799e1fa886410bb64c Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 11:58:06 -0800 Subject: [PATCH 05/28] add sense service to android manifest --- ble/src/main/AndroidManifest.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ble/src/main/AndroidManifest.xml b/ble/src/main/AndroidManifest.xml index 2245e34..024e99e 100644 --- a/ble/src/main/AndroidManifest.xml +++ b/ble/src/main/AndroidManifest.xml @@ -5,4 +5,13 @@ + + + + + + From 2035b7b5b26471f25b2eee9c55f950c2cd419fa7 Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 15:10:31 -0800 Subject: [PATCH 06/28] bump minimum SDK version; add task serialization to SenseService --- ble/build.gradle | 4 +- .../commonsense/service/SenseService.java | 77 +++++++++++++++---- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/ble/build.gradle b/ble/build.gradle index c1ec524..7a90c2d 100644 --- a/ble/build.gradle +++ b/ble/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion "23.0.1" defaultConfig { - minSdkVersion 18 + minSdkVersion 19 targetSdkVersion 23 versionCode generateVersionCode() versionName VERSION_NAME @@ -40,7 +40,7 @@ android { dependencies { compile 'io.reactivex:rxjava:1.0.9' compile 'com.android.support:support-v4:23.1.1' - compile 'is.hello:buruberi-core:1.0.1' + compile 'is.hello:buruberi-core:1.0.2' compile fileTree(dir: 'libs', include: ['*.jar']) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index a27e173..32ecfea 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -1,19 +1,28 @@ package is.hello.commonsense.service; import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.os.Binder; import android.os.IBinder; import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; import java.util.List; +import java.util.Objects; import is.hello.buruberi.bluetooth.errors.ConnectionStateException; +import is.hello.buruberi.bluetooth.stacks.BluetoothStack; import is.hello.buruberi.bluetooth.stacks.GattPeripheral; import is.hello.buruberi.bluetooth.stacks.util.AdvertisingData; import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; +import is.hello.buruberi.util.Rx; +import is.hello.buruberi.util.SerialQueue; import is.hello.commonsense.bluetooth.SenseIdentifiers; import is.hello.commonsense.bluetooth.SensePeripheral; import is.hello.commonsense.bluetooth.model.SenseConnectToWiFiUpdate; @@ -23,9 +32,11 @@ import is.hello.commonsense.util.ConnectProgress; import is.hello.commonsense.util.Functions; import rx.Observable; -import rx.functions.Action0; public class SenseService extends Service { + private static final String LOG_TAG = SenseService.class.getSimpleName(); + + private final SerialQueue queue = new SerialQueue(); private @Nullable SensePeripheral sense; //region Service Lifecycle @@ -48,13 +59,37 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { super.onCreate(); + + final IntentFilter intentFilter = new IntentFilter(GattPeripheral.ACTION_DISCONNECTED); + LocalBroadcastManager.getInstance(this) + .registerReceiver(peripheralDisconnected, intentFilter); } @Override public void onDestroy() { super.onDestroy(); + + LocalBroadcastManager.getInstance(this) + .unregisterReceiver(peripheralDisconnected); + + if (sense != null && sense.isConnected()) { + Log.w(LOG_TAG, "Service being destroyed with active connection"); + } } + private final BroadcastReceiver peripheralDisconnected = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (sense != null) { + final String intentAddress = intent.getStringExtra(GattPeripheral.EXTRA_ADDRESS); + final String senseAddress = sense.getAddress(); + if (Objects.equals(intentAddress, senseAddress)) { + onPeripheralDisconnected(); + } + } + } + }; + //endregion @@ -69,6 +104,11 @@ private static Exception createNoDeviceException() { //region Managing Connectivity + private void onPeripheralDisconnected() { + this.sense = null; + queue.cancelPending(createNoDeviceException()); + } + public void preparePeripheralCriteria(@NonNull PeripheralCriteria criteria, @Nullable String deviceId) { criteria.addExactMatchPredicate(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, @@ -80,6 +120,13 @@ public void preparePeripheralCriteria(@NonNull PeripheralCriteria criteria, } } + public Observable> discover(@NonNull BluetoothStack onStack, + @Nullable String deviceId) { + final PeripheralCriteria criteria = new PeripheralCriteria(); + preparePeripheralCriteria(criteria, deviceId); + return onStack.discoverPeripherals(criteria); + } + @CheckResult public Observable connect(@NonNull GattPeripheral peripheral) { if (this.sense != null) { @@ -91,6 +138,7 @@ public Observable connect(@NonNull GattPeripheral peripheral) { return Observable.just(ConnectProgress.CONNECTED); } + // Intentionally not serialized on #queue return sense.connect(); } @@ -100,15 +148,9 @@ public Observable disconnect() { return Observable.just(null); } + // Intentionally not serialized on #queue return sense.disconnect() - .map(Functions.createMapperToVoid()) - .doOnTerminate(new Action0() { - @CheckResult - @Override - public void call() { - SenseService.this.sense = null; - } - }); + .map(Functions.createMapperToVoid()); } @CheckResult @@ -117,6 +159,7 @@ public Observable removeBond() { return Observable.error(createNoDeviceException()); } + // Intentionally not serialized on #queue return sense.removeBond() .map(Functions.createMapperToVoid()); } @@ -132,7 +175,7 @@ public Observable runLedAnimation(@NonNull SenseLedAnimation animationType return Observable.error(createNoDeviceException()); } - return sense.runLedAnimation(animationType); + return Rx.serialize(sense.runLedAnimation(animationType), queue); } @CheckResult @@ -141,7 +184,7 @@ public Observable> scanForWifiNetworks(@Nullable SensePeriph return Observable.error(createNoDeviceException()); } - return sense.scanForWifiNetworks(countryCode); + return Rx.serialize(sense.scanForWifiNetworks(countryCode), queue); } @CheckResult @@ -161,7 +204,7 @@ public Observable sendWifiCredentials(@NonNull String return Observable.error(createNoDeviceException()); } - return sense.connectToWiFiNetwork(ssid, securityType, password); + return Rx.serialize(sense.connectToWiFiNetwork(ssid, securityType, password), queue); } @CheckResult @@ -170,7 +213,7 @@ public Observable linkAccount(@NonNull String accessToken) { return Observable.error(createNoDeviceException()); } - return sense.linkAccount(accessToken); + return Rx.serialize(sense.linkAccount(accessToken), queue); } @CheckResult @@ -179,7 +222,7 @@ public Observable linkPill(@NonNull String accessToken) { return Observable.error(createNoDeviceException()); } - return sense.pairPill(accessToken); + return Rx.serialize(sense.pairPill(accessToken), queue); } @CheckResult @@ -188,7 +231,7 @@ public Observable pushData() { return Observable.error(createNoDeviceException()); } - return sense.pushData(); + return Rx.serialize(sense.pushData(), queue); } @CheckResult @@ -197,7 +240,7 @@ public Observable putIntoPairingMode() { return Observable.error(createNoDeviceException()); } - return sense.putIntoPairingMode(); + return Rx.serialize(sense.putIntoPairingMode(), queue); } @CheckResult @@ -206,7 +249,7 @@ public Observable factoryReset() { return Observable.error(createNoDeviceException()); } - return sense.factoryReset(); + return Rx.serialize(sense.factoryReset(), queue); } //endregion From a129425cee8e0bfdef515876e85846495c829668 Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 16:26:25 -0800 Subject: [PATCH 07/28] separate repeated mock creation into utility class; add some tests for SenseService --- .../commonsense/service/SenseService.java | 44 ++-- .../test/java/is/hello/commonsense/Mocks.java | 67 ++++++ .../bluetooth/SensePeripheralTests.java | 212 +++++++----------- .../service/SenseServiceTests.java | 155 ++++++++++++- 4 files changed, 313 insertions(+), 165 deletions(-) create mode 100644 ble/src/test/java/is/hello/commonsense/Mocks.java diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 32ecfea..433b6c7 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -10,6 +10,7 @@ import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; @@ -17,7 +18,6 @@ import java.util.Objects; import is.hello.buruberi.bluetooth.errors.ConnectionStateException; -import is.hello.buruberi.bluetooth.stacks.BluetoothStack; import is.hello.buruberi.bluetooth.stacks.GattPeripheral; import is.hello.buruberi.bluetooth.stacks.util.AdvertisingData; import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; @@ -36,8 +36,8 @@ public class SenseService extends Service { private static final String LOG_TAG = SenseService.class.getSimpleName(); - private final SerialQueue queue = new SerialQueue(); - private @Nullable SensePeripheral sense; + @VisibleForTesting final SerialQueue queue = new SerialQueue(); + @VisibleForTesting @Nullable SensePeripheral sense; //region Service Lifecycle @@ -77,19 +77,6 @@ public void onDestroy() { } } - private final BroadcastReceiver peripheralDisconnected = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (sense != null) { - final String intentAddress = intent.getStringExtra(GattPeripheral.EXTRA_ADDRESS); - final String senseAddress = sense.getAddress(); - if (Objects.equals(intentAddress, senseAddress)) { - onPeripheralDisconnected(); - } - } - } - }; - //endregion @@ -104,6 +91,19 @@ private static Exception createNoDeviceException() { //region Managing Connectivity + private final BroadcastReceiver peripheralDisconnected = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (sense != null) { + final String intentAddress = intent.getStringExtra(GattPeripheral.EXTRA_ADDRESS); + final String senseAddress = sense.getAddress(); + if (Objects.equals(intentAddress, senseAddress)) { + onPeripheralDisconnected(); + } + } + } + }; + private void onPeripheralDisconnected() { this.sense = null; queue.cancelPending(createNoDeviceException()); @@ -120,13 +120,6 @@ public void preparePeripheralCriteria(@NonNull PeripheralCriteria criteria, } } - public Observable> discover(@NonNull BluetoothStack onStack, - @Nullable String deviceId) { - final PeripheralCriteria criteria = new PeripheralCriteria(); - preparePeripheralCriteria(criteria, deviceId); - return onStack.discoverPeripherals(criteria); - } - @CheckResult public Observable connect(@NonNull GattPeripheral peripheral) { if (this.sense != null) { @@ -134,9 +127,6 @@ public Observable connect(@NonNull GattPeripheral peripheral) { } this.sense = new SensePeripheral(peripheral); - if (sense.isConnected()) { - return Observable.just(ConnectProgress.CONNECTED); - } // Intentionally not serialized on #queue return sense.connect(); @@ -199,7 +189,7 @@ public Observable currentWifiNetwork() { @CheckResult public Observable sendWifiCredentials(@NonNull String ssid, @NonNull wifi_endpoint.sec_type securityType, - @NonNull String password) { + @Nullable String password) { if (sense == null) { return Observable.error(createNoDeviceException()); } diff --git a/ble/src/test/java/is/hello/commonsense/Mocks.java b/ble/src/test/java/is/hello/commonsense/Mocks.java new file mode 100644 index 0000000..25f8e1f --- /dev/null +++ b/ble/src/test/java/is/hello/commonsense/Mocks.java @@ -0,0 +1,67 @@ +package is.hello.commonsense; + +import android.support.annotation.NonNull; + +import java.util.UUID; + +import is.hello.buruberi.bluetooth.stacks.BluetoothStack; +import is.hello.buruberi.bluetooth.stacks.GattCharacteristic; +import is.hello.buruberi.bluetooth.stacks.GattPeripheral; +import is.hello.buruberi.bluetooth.stacks.GattService; +import is.hello.buruberi.bluetooth.stacks.util.LoggerFacade; +import is.hello.commonsense.bluetooth.SenseIdentifiers; +import rx.schedulers.Schedulers; + +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@SuppressWarnings("ResourceType") +public final class Mocks { + public static final String DEVICE_ID = "CA154FFA"; + + public static BluetoothStack createBluetoothStack() { + final BluetoothStack stack = mock(BluetoothStack.class); + doReturn(Schedulers.immediate()) + .when(stack) + .getScheduler(); + doReturn(mock(LoggerFacade.class, CALLS_REAL_METHODS)) + .when(stack) + .getLogger(); + return stack; + } + + public static GattPeripheral createPeripheral(@NonNull BluetoothStack stack) { + final GattPeripheral device = mock(GattPeripheral.class); + doReturn(stack) + .when(device) + .getStack(); + doReturn(DEVICE_ID) + .when(device) + .getAddress(); + return device; + } + + public static GattService createGattService() { + final GattService service = mock(GattService.class); + doReturn(SenseIdentifiers.SERVICE) + .when(service) + .getUuid(); + doReturn(GattService.TYPE_PRIMARY) + .when(service) + .getType(); + return service; + } + + public static GattCharacteristic createGattCharacteristic(@NonNull GattService service, + @NonNull UUID uuid) { + final GattCharacteristic characteristic = mock(GattCharacteristic.class); + doReturn(service) + .when(characteristic) + .getService(); + doReturn(uuid) + .when(characteristic) + .getUuid(); + return characteristic; + } +} diff --git a/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java index 29808aa..b062240 100644 --- a/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java +++ b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java @@ -1,7 +1,6 @@ package is.hello.commonsense.bluetooth; import android.bluetooth.BluetoothGatt; -import android.support.annotation.NonNull; import org.junit.Test; @@ -19,15 +18,14 @@ import is.hello.buruberi.bluetooth.stacks.GattService; import is.hello.buruberi.bluetooth.stacks.OperationTimeout; import is.hello.buruberi.bluetooth.stacks.util.AdvertisingData; -import is.hello.buruberi.bluetooth.stacks.util.LoggerFacade; import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; import is.hello.buruberi.util.AdvertisingDataBuilder; import is.hello.buruberi.util.Operation; import is.hello.commonsense.CommonSenseTestCase; +import is.hello.commonsense.Mocks; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos; import is.hello.commonsense.util.Sync; import rx.Observable; -import rx.schedulers.Schedulers; import static is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.MorpheusCommand; import static org.hamcrest.Matchers.equalTo; @@ -37,7 +35,6 @@ import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -46,73 +43,24 @@ @SuppressWarnings("ResourceType") public class SensePeripheralTests extends CommonSenseTestCase { - private static final String TEST_DEVICE_ID = "CA154FFA"; - - //region Vending Mocks - - static BluetoothStack createMockBluetoothStack() { - BluetoothStack stack = mock(BluetoothStack.class); - doReturn(Schedulers.immediate()) - .when(stack) - .getScheduler(); - doReturn(mock(LoggerFacade.class, CALLS_REAL_METHODS)) - .when(stack) - .getLogger(); - return stack; - } - - static GattPeripheral createMockPeripheral(@NonNull BluetoothStack stack) { - final GattPeripheral device = mock(GattPeripheral.class); - doReturn(stack) - .when(device) - .getStack(); - return device; - } - - static GattService createMockGattService() { - GattService service = mock(GattService.class); - doReturn(SenseIdentifiers.SERVICE) - .when(service) - .getUuid(); - doReturn(GattService.TYPE_PRIMARY) - .when(service) - .getType(); - return service; - } - - static GattCharacteristic createMockGattCharacteristic(@NonNull GattService service, - @NonNull UUID uuid) { - final GattCharacteristic characteristic = mock(GattCharacteristic.class); - doReturn(service) - .when(characteristic) - .getService(); - doReturn(uuid) - .when(characteristic) - .getUuid(); - return characteristic; - } - - //endregion - - //region Discovery @Test public void discovery() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); + final BluetoothStack stack = Mocks.createBluetoothStack(); final AdvertisingDataBuilder builder = new AdvertisingDataBuilder(); builder.add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT); final AdvertisingData advertisingData = builder.build(); - final GattPeripheral device1 = createMockPeripheral(stack); + final GattPeripheral device1 = Mocks.createPeripheral(stack); doReturn("Sense-Test").when(device1).getName(); doReturn("ca:15:4f:fa:b7:0b").when(device1).getAddress(); doReturn(-50).when(device1).getScanTimeRssi(); doReturn(advertisingData).when(device1).getAdvertisingData(); - final GattPeripheral device2 = createMockPeripheral(stack); + final GattPeripheral device2 = Mocks.createPeripheral(stack); doReturn("Sense-Test2").when(device2).getName(); doReturn("c2:18:4e:fb:b3:0a").when(device2).getAddress(); doReturn(-90).when(device2).getScanTimeRssi(); @@ -132,14 +80,14 @@ public void discovery() throws Exception { @Test public void rediscovery() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); + final BluetoothStack stack = Mocks.createBluetoothStack(); final AdvertisingDataBuilder builder = new AdvertisingDataBuilder(); builder.add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT); - builder.add(AdvertisingData.TYPE_SERVICE_DATA, SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + TEST_DEVICE_ID); + builder.add(AdvertisingData.TYPE_SERVICE_DATA, SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + Mocks.DEVICE_ID); final AdvertisingData advertisingData = builder.build(); - final GattPeripheral device = createMockPeripheral(stack); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn("Sense-Test").when(device).getName(); doReturn("ca:15:4f:fa:b7:0b").when(device).getAddress(); doReturn(-50).when(device).getScanTimeRssi(); @@ -151,7 +99,7 @@ public void rediscovery() throws Exception { .when(stack) .discoverPeripherals(any(PeripheralCriteria.class)); - SensePeripheral peripheral = Sync.last(SensePeripheral.rediscover(stack, TEST_DEVICE_ID, false)); + SensePeripheral peripheral = Sync.last(SensePeripheral.rediscover(stack, Mocks.DEVICE_ID, false)); assertThat(peripheral.getName(), is(equalTo("Sense-Test"))); } @@ -162,14 +110,14 @@ public void rediscovery() throws Exception { @Test public void isConnected() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(GattPeripheral.STATUS_CONNECTED).when(device).getConnectionStatus(); final SensePeripheral peripheral = new SensePeripheral(device); assertThat(peripheral.isConnected(), is(false)); - peripheral.gattService = createMockGattService(); + peripheral.gattService = Mocks.createGattService(); assertThat(peripheral.isConnected(), is(true)); doReturn(GattPeripheral.STATUS_DISCONNECTED).when(device).getConnectionStatus(); @@ -184,8 +132,8 @@ public void isConnected() throws Exception { @Test public void getBondStatus() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); final SensePeripheral peripheral = new SensePeripheral(device); doReturn(GattPeripheral.BOND_NONE).when(device).getBondStatus(); @@ -200,8 +148,8 @@ public void getBondStatus() throws Exception { @Test public void getScannedRssi() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(-50).when(device).getScanTimeRssi(); final SensePeripheral peripheral = new SensePeripheral(device); @@ -210,8 +158,8 @@ public void getScannedRssi() throws Exception { @Test public void getAddress() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn("ca:15:4f:fa:b7:0b").when(device).getAddress(); final SensePeripheral peripheral = new SensePeripheral(device); @@ -220,8 +168,8 @@ public void getAddress() throws Exception { @Test public void getName() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn("Sense-Test").when(device).getName(); final SensePeripheral peripheral = new SensePeripheral(device); @@ -234,17 +182,17 @@ public void getDeviceId() throws Exception { builder.add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT); builder.add(AdvertisingData.TYPE_SERVICE_DATA, - SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + TEST_DEVICE_ID); + SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + Mocks.DEVICE_ID); AdvertisingData advertisingData = builder.build(); - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(advertisingData) .when(device) .getAdvertisingData(); final SensePeripheral peripheral = new SensePeripheral(device); - assertEquals(TEST_DEVICE_ID, peripheral.getDeviceId()); + assertEquals(Mocks.DEVICE_ID, peripheral.getDeviceId()); } //endregion @@ -254,8 +202,8 @@ public void getDeviceId() throws Exception { @Test public void connectSucceeded() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); //noinspection ResourceType doReturn(Observable.just(device)) .when(device) @@ -283,8 +231,8 @@ public void connectSucceeded() throws Exception { @Test public void connectFailedFromConnect() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); //noinspection ResourceType doReturn(Observable.error(new UserDisabledBuruberiException())) .when(device) @@ -307,8 +255,8 @@ public void connectFailedFromConnect() throws Exception { @Test public void connectFailedFromCreateBond() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); //noinspection ResourceType doReturn(Observable.error(new UserDisabledBuruberiException())) .when(device) @@ -331,8 +279,8 @@ public void connectFailedFromCreateBond() throws Exception { @Test public void connectFailedFromDiscoverService() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); //noinspection ResourceType doReturn(Observable.error(new UserDisabledBuruberiException())) .when(device) @@ -356,8 +304,8 @@ public void connectFailedFromDiscoverService() throws Exception { @Test public void disconnectSuccess() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(Observable.just(device)) .when(device) .disconnect(); @@ -370,8 +318,8 @@ public void disconnectSuccess() throws Exception { @Test public void disconnectFailure() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(Observable.error(new GattException(BluetoothGatt.GATT_FAILURE, Operation.DISCONNECT))) .when(device) @@ -393,19 +341,19 @@ public void subscribeResponseSuccess() throws Exception { final UUID characteristicId = SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE; final UUID descriptorId = SenseIdentifiers.DESCRIPTOR_CHARACTERISTIC_COMMAND_RESPONSE_CONFIG; - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(GattPeripheral.STATUS_CONNECTED) .when(device) .getConnectionStatus(); final SensePeripheral peripheral = new SensePeripheral(device); - peripheral.gattService = createMockGattService(); - peripheral.commandCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); - peripheral.responseCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); + peripheral.gattService = Mocks.createGattService(); + peripheral.commandCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); + peripheral.responseCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); doReturn(Observable.just(characteristicId)) .when(peripheral.responseCharacteristic) .enableNotification(eq(descriptorId), @@ -437,19 +385,19 @@ public void subscribeResponseSuccess() throws Exception { public void subscribeResponseFailure() throws Exception { final UUID descriptorId = SenseIdentifiers.DESCRIPTOR_CHARACTERISTIC_COMMAND_RESPONSE_CONFIG; - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(GattPeripheral.STATUS_CONNECTED) .when(device) .getConnectionStatus(); final SensePeripheral peripheral = new SensePeripheral(device); - peripheral.gattService = createMockGattService(); - peripheral.commandCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); - peripheral.responseCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); + peripheral.gattService = Mocks.createGattService(); + peripheral.commandCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); + peripheral.responseCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); doReturn(Observable.error(new GattException(BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION, Operation.ENABLE_NOTIFICATION))) .when(peripheral.responseCharacteristic) @@ -465,19 +413,19 @@ public void unsubscribeResponseSuccess() throws Exception { final UUID characteristicId = SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE; final UUID descriptorId = SenseIdentifiers.DESCRIPTOR_CHARACTERISTIC_COMMAND_RESPONSE_CONFIG; - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(GattPeripheral.STATUS_CONNECTED) .when(device) .getConnectionStatus(); final SensePeripheral peripheral = new SensePeripheral(device); - peripheral.gattService = createMockGattService(); - peripheral.commandCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); - peripheral.responseCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); + peripheral.gattService = Mocks.createGattService(); + peripheral.commandCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); + peripheral.responseCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); doReturn(Observable.just(characteristicId)) .when(peripheral.responseCharacteristic) .disableNotification(eq(descriptorId), @@ -501,19 +449,19 @@ public void unsubscribeResponseSuccess() throws Exception { public void unsubscribeResponseFailure() throws Exception { final UUID descriptorId = SenseIdentifiers.DESCRIPTOR_CHARACTERISTIC_COMMAND_RESPONSE_CONFIG; - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(GattPeripheral.STATUS_CONNECTED) .when(device) .getConnectionStatus(); final SensePeripheral peripheral = new SensePeripheral(device); - peripheral.gattService = createMockGattService(); - peripheral.commandCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); - peripheral.responseCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); + peripheral.gattService = Mocks.createGattService(); + peripheral.commandCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); + peripheral.responseCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); doReturn(Observable.error(new GattException(BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION, Operation.ENABLE_NOTIFICATION))) .when(peripheral.responseCharacteristic) @@ -526,15 +474,15 @@ public void unsubscribeResponseFailure() throws Exception { @Test public void unsubscribeResponseNoConnection() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); doReturn(GattPeripheral.STATUS_DISCONNECTED) .when(device) .getConnectionStatus(); final SensePeripheral peripheral = new SensePeripheral(device); - peripheral.gattService = createMockGattService(); + peripheral.gattService = Mocks.createGattService(); Sync.wrap(peripheral.unsubscribeResponse(mock(OperationTimeout.class))) .assertThat(is(equalTo(SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE))); @@ -547,15 +495,15 @@ public void unsubscribeResponseNoConnection() throws Exception { @Test public void writeLargeCommandSuccess() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); final SensePeripheral peripheral = new SensePeripheral(device); - peripheral.gattService = createMockGattService(); - peripheral.commandCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); - peripheral.responseCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); + peripheral.gattService = Mocks.createGattService(); + peripheral.commandCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); + peripheral.responseCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); doReturn(Observable.just(null)) .when(peripheral.commandCharacteristic) .write(any(GattPeripheral.WriteType.class), @@ -579,15 +527,15 @@ public void writeLargeCommandSuccess() throws Exception { @Test public void writeLargeCommandFailure() throws Exception { - final BluetoothStack stack = createMockBluetoothStack(); - final GattPeripheral device = createMockPeripheral(stack); + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); final SensePeripheral peripheral = new SensePeripheral(device); - peripheral.gattService = createMockGattService(); - peripheral.commandCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); - peripheral.responseCharacteristic = createMockGattCharacteristic(peripheral.gattService, - SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); + peripheral.gattService = Mocks.createGattService(); + peripheral.commandCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); + peripheral.responseCharacteristic = Mocks.createGattCharacteristic(peripheral.gattService, + SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); doReturn(Observable.error(new GattException(GattException.GATT_STACK_ERROR, Operation.WRITE_COMMAND))) diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index f724d87..f22252b 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -4,18 +4,36 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; +import android.support.v4.content.LocalBroadcastManager; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.robolectric.shadows.ShadowApplication; +import java.util.concurrent.atomic.AtomicInteger; + +import is.hello.buruberi.bluetooth.errors.ConnectionStateException; +import is.hello.buruberi.bluetooth.stacks.BluetoothStack; +import is.hello.buruberi.bluetooth.stacks.GattPeripheral; +import is.hello.buruberi.bluetooth.stacks.util.AdvertisingData; +import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; +import is.hello.buruberi.util.AdvertisingDataBuilder; +import is.hello.buruberi.util.SerialQueue; import is.hello.commonsense.CommonSenseTestCase; +import is.hello.commonsense.Mocks; +import is.hello.commonsense.bluetooth.SenseIdentifiers; +import is.hello.commonsense.bluetooth.SensePeripheral; +import is.hello.commonsense.bluetooth.model.SenseLedAnimation; +import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_endpoint.sec_type; +import is.hello.commonsense.util.Sync; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -44,13 +62,9 @@ public void tearDown() throws Exception { //endregion - static SenseServiceConnection.Listener createMockConsumer() { - return mock(SenseServiceConnection.Listener.class); - } - @Test - public void connection() { - final SenseServiceConnection.Listener listener = createMockConsumer(); + public void serviceConnection() { + final SenseServiceConnection.Listener listener = mock(SenseServiceConnection.Listener.class); final SenseServiceConnection helper = new SenseServiceConnection(getContext()); helper.registerConsumer(listener); helper.create(); @@ -63,4 +77,133 @@ public void connection() { assertThat(helper.getSenseService(), is(nullValue())); verify(listener).onSenseServiceDisconnected(); } + + @Test + public void preparePeripheralCriteria() { + final AdvertisingData withoutDeviceId = new AdvertisingDataBuilder() + .add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT) + .build(); + + final PeripheralCriteria criteriaWithoutDeviceId = new PeripheralCriteria(); + service.preparePeripheralCriteria(criteriaWithoutDeviceId, null); + assertThat(criteriaWithoutDeviceId.matches(withoutDeviceId), is(true)); + + + final AdvertisingData withDeviceId = new AdvertisingDataBuilder() + .add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT) + .add(AdvertisingData.TYPE_SERVICE_DATA, + SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + Mocks.DEVICE_ID) + .build(); + + final PeripheralCriteria criteriaWithDeviceId = new PeripheralCriteria(); + service.preparePeripheralCriteria(criteriaWithDeviceId, Mocks.DEVICE_ID); + assertThat(criteriaWithDeviceId.limit, is(equalTo(1))); + assertThat(criteriaWithDeviceId.matches(withDeviceId), is(true)); + } + + @Test(expected = IllegalStateException.class) + public void connectSingleDeviceOnly() { + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); + service.sense = new SensePeripheral(device); + Sync.last(service.connect(device)); // should throw + } + + @Test + public void peripheralDisconnected() { + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); + service.sense = new SensePeripheral(device); + + service.queue.execute(new SerialQueue.Task() { + @Override + public void cancel(Throwable throwable) { + // This will never be called. + } + + @Override + public void run() { + // Do nothing. This task is just to block the queue. + } + }); + final AtomicInteger cancelCount = new AtomicInteger(0); + service.queue.execute(new SerialQueue.Task() { + @Override + public void cancel(Throwable throwable) { + cancelCount.incrementAndGet(); + } + + @Override + public void run() { + fail(); + } + }); + + final Intent disconnected = new Intent(GattPeripheral.ACTION_DISCONNECTED) + .putExtra(GattPeripheral.EXTRA_ADDRESS, Mocks.DEVICE_ID); + LocalBroadcastManager.getInstance(getContext()) + .sendBroadcastSync(disconnected); + + assertThat(service.sense, is(nullValue())); + assertThat(cancelCount.get(), is(equalTo(1))); + } + + @Test + public void disconnectWithNoDevice() { + assertThat(Sync.last(service.disconnect()), is(equalTo(null))); + } + + @Test(expected = ConnectionStateException.class) + public void removeBondRequiresDevice() { + Sync.last(service.removeBond()); + } + + @Test(expected = ConnectionStateException.class) + public void runLedAnimationRequiresDevice() { + Sync.last(service.runLedAnimation(SenseLedAnimation.BUSY)); + } + + @Test(expected = ConnectionStateException.class) + public void scanForWifiNetworksRequiresDevice() { + Sync.last(service.scanForWifiNetworks(null)); + } + + @Test(expected = ConnectionStateException.class) + public void currentWifiNetworkRequiresDevice() { + Sync.last(service.currentWifiNetwork()); + } + + @Test(expected = ConnectionStateException.class) + public void sendWifiCredentialsRequiresDevice() { + Sync.last(service.sendWifiCredentials("Hello", + sec_type.SL_SCAN_SEC_TYPE_OPEN, + null)); + } + + @Test(expected = ConnectionStateException.class) + public void linkAccountRequiresDevice() { + Sync.last(service.linkAccount("token")); + } + + @Test(expected = ConnectionStateException.class) + public void linkPillRequiresDevice() { + Sync.last(service.linkPill("token")); + } + + @Test(expected = ConnectionStateException.class) + public void pushDataRequiresDevice() { + Sync.last(service.pushData()); + } + + @Test(expected = ConnectionStateException.class) + public void putIntoPairingModeRequiresDevice() { + Sync.last(service.putIntoPairingMode()); + } + + @Test(expected = ConnectionStateException.class) + public void factoryResetRequiresDevice() { + Sync.last(service.factoryReset()); + } } From 1b0e1517aaffe9a95662a2a1bd469fcb80c619bd Mon Sep 17 00:00:00 2001 From: km Date: Wed, 24 Feb 2016 16:27:13 -0800 Subject: [PATCH 08/28] bump library version --- ble/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ble/build.gradle b/ble/build.gradle index 7a90c2d..8240318 100644 --- a/ble/build.gradle +++ b/ble/build.gradle @@ -1,7 +1,7 @@ import java.text.SimpleDateFormat // Used for both the 'aar' file and publish. -def VERSION_NAME = "2016.2.12.1" // ... +def VERSION_NAME = "2016.2.24.1" // ... def PACKAGE = 'is.hello.commonsense' apply plugin: 'com.android.library' From 0f919dc465aa0b8149d1426892e277d7aa77db6a Mon Sep 17 00:00:00 2001 From: km Date: Thu, 25 Feb 2016 11:04:06 -0800 Subject: [PATCH 09/28] scan criteria doesn't require instance state --- .../java/is/hello/commonsense/service/SenseService.java | 4 ++-- .../is/hello/commonsense/service/SenseServiceTests.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 433b6c7..da83eee 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -109,8 +109,8 @@ private void onPeripheralDisconnected() { queue.cancelPending(createNoDeviceException()); } - public void preparePeripheralCriteria(@NonNull PeripheralCriteria criteria, - @Nullable String deviceId) { + public static void prepareForScan(@NonNull PeripheralCriteria criteria, + @Nullable String deviceId) { criteria.addExactMatchPredicate(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT); if (deviceId != null) { diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index f22252b..b95f6c5 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -79,14 +79,14 @@ public void serviceConnection() { } @Test - public void preparePeripheralCriteria() { + public void prepareForScan() { final AdvertisingData withoutDeviceId = new AdvertisingDataBuilder() .add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT) .build(); final PeripheralCriteria criteriaWithoutDeviceId = new PeripheralCriteria(); - service.preparePeripheralCriteria(criteriaWithoutDeviceId, null); + SenseService.prepareForScan(criteriaWithoutDeviceId, null); assertThat(criteriaWithoutDeviceId.matches(withoutDeviceId), is(true)); @@ -98,7 +98,7 @@ public void preparePeripheralCriteria() { .build(); final PeripheralCriteria criteriaWithDeviceId = new PeripheralCriteria(); - service.preparePeripheralCriteria(criteriaWithDeviceId, Mocks.DEVICE_ID); + SenseService.prepareForScan(criteriaWithDeviceId, Mocks.DEVICE_ID); assertThat(criteriaWithDeviceId.limit, is(equalTo(1))); assertThat(criteriaWithDeviceId.matches(withDeviceId), is(true)); } From d1c2231657b840cd23502b2c33e7ad593e326c98 Mon Sep 17 00:00:00 2001 From: km Date: Thu, 25 Feb 2016 11:33:42 -0800 Subject: [PATCH 10/28] add observable interface to SenseServiceConnection; add convenience methods for connected, bond status --- .../commonsense/service/SenseService.java | 12 ++++++++ .../service/SenseServiceConnection.java | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index da83eee..7335bf6 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -154,6 +154,18 @@ public Observable removeBond() { .map(Functions.createMapperToVoid()); } + public boolean isConnected() { + return (sense != null && sense.isConnected()); + } + + public int getBondStatus() { + if (sense == null) { + return GattPeripheral.BOND_NONE; + } else { + return sense.getBondStatus(); + } + } + //endregion diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java index 67cce1d..51e9430 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java @@ -11,6 +11,9 @@ import java.util.ArrayList; import java.util.List; +import rx.Observable; +import rx.subjects.AsyncSubject; + /** * Helper class to facilitate communication between a component and the {@link SenseService}. */ @@ -104,6 +107,33 @@ public SenseService getSenseService() { return senseService; } + /** + * Creates an {@code Observable} that will produce the + * {@link SenseService} as soon as it becomes available. + */ + public Observable senseService() { + if (senseService != null) { + return Observable.just(senseService); + } else { + final AsyncSubject mirror = AsyncSubject.create(); + registerConsumer(new Listener() { + @Override + public void onSenseServiceConnected(@NonNull SenseService service) { + mirror.onNext(service); + mirror.onCompleted(); + + unregisterConsumer(this); + } + + @Override + public void onSenseServiceDisconnected() { + // Do nothing. + } + }); + return mirror; + } + } + //endregion From 857640f1c29964ea9817f7c5b900a57243d94dc6 Mon Sep 17 00:00:00 2001 From: km Date: Thu, 25 Feb 2016 11:58:57 -0800 Subject: [PATCH 11/28] yield SenseService for effectful operations to make chaining simpler; refactor Functions --- .../bluetooth/SensePeripheral.java | 26 ++++++------ .../commonsense/service/SenseService.java | 40 +++++++++++-------- .../util/{Functions.java => Func.java} | 6 +-- 3 files changed, 39 insertions(+), 33 deletions(-) rename ble/src/main/java/is/hello/commonsense/util/{Functions.java => Func.java} (70%) diff --git a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java index e698f7a..b8c5e72 100644 --- a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java +++ b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java @@ -42,7 +42,7 @@ import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_endpoint; import is.hello.commonsense.util.ConnectProgress; -import is.hello.commonsense.util.Functions; +import is.hello.commonsense.util.Func; import rx.Observable; import rx.Observer; import rx.Subscriber; @@ -204,15 +204,15 @@ public ConnectProgress call(GattService service) { // bonds without an active connection. sequence = Observable.concat( Observable.just(ConnectProgress.BONDING), - gattPeripheral.createBond().map(Functions.createMapperToValue(ConnectProgress.CONNECTING)), - gattPeripheral.connect(connectFlags, timeout).map(Functions.createMapperToValue(ConnectProgress.DISCOVERING_SERVICES)), + gattPeripheral.createBond().map(Func.justValue(ConnectProgress.CONNECTING)), + gattPeripheral.connect(connectFlags, timeout).map(Func.justValue(ConnectProgress.DISCOVERING_SERVICES)), gattPeripheral.discoverService(SenseIdentifiers.SERVICE, timeout).map(onDiscoveredServices) ); } else { sequence = Observable.concat( Observable.just(ConnectProgress.CONNECTING), - gattPeripheral.connect(connectFlags, timeout).map(Functions.createMapperToValue(ConnectProgress.BONDING)), - gattPeripheral.createBond().map(Functions.createMapperToValue(ConnectProgress.DISCOVERING_SERVICES)), + gattPeripheral.connect(connectFlags, timeout).map(Func.justValue(ConnectProgress.BONDING)), + gattPeripheral.createBond().map(Func.justValue(ConnectProgress.DISCOVERING_SERVICES)), gattPeripheral.discoverService(SenseIdentifiers.SERVICE, timeout).map(onDiscoveredServices) ); } @@ -252,7 +252,7 @@ public void onNext(SensePeripheral sensePeripheral) { @CheckResult public Observable disconnect() { return gattPeripheral.disconnect() - .map(Functions.createMapperToValue(this)) + .map(Func.justValue(this)) .finallyDo(new Action0() { @Override public void call() { @@ -269,7 +269,7 @@ public Observable removeBond() { REMOVE_BOND_TIMEOUT_S, TimeUnit.SECONDS); return gattPeripheral.removeBond(timeout) - .map(Functions.createMapperToValue(this)); + .map(Func.justValue(this)); } //endregion @@ -624,7 +624,7 @@ public Observable putIntoNormalMode() { .setAppVersion(APP_VERSION) .build(); return performSimpleCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -642,7 +642,7 @@ public Observable putIntoPairingMode() { .setAppVersion(APP_VERSION) .build(); return performDisconnectingCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -870,7 +870,7 @@ public Observable linkAccount(final String accountToken) { .setAccountId(accountToken) .build(); return performSimpleCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -888,7 +888,7 @@ public Observable factoryReset() { .setAppVersion(APP_VERSION) .build(); return performDisconnectingCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -906,7 +906,7 @@ public Observable pushData() { .setAppVersion(APP_VERSION) .build(); return performSimpleCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -924,7 +924,7 @@ public Observable runLedAnimation(@NonNull SenseLedAnimation animationType .setAppVersion(APP_VERSION) .build(); return performSimpleCommand(morpheusCommand, createAnimationTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 7335bf6..1b75377 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -30,7 +30,7 @@ import is.hello.commonsense.bluetooth.model.SenseNetworkStatus; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_endpoint; import is.hello.commonsense.util.ConnectProgress; -import is.hello.commonsense.util.Functions; +import is.hello.commonsense.util.Func; import rx.Observable; public class SenseService extends Service { @@ -133,25 +133,25 @@ public Observable connect(@NonNull GattPeripheral peripheral) { } @CheckResult - public Observable disconnect() { + public Observable disconnect() { if (sense == null) { return Observable.just(null); } // Intentionally not serialized on #queue return sense.disconnect() - .map(Functions.createMapperToVoid()); + .map(Func.justValue(this)); } @CheckResult - public Observable removeBond() { + public Observable removeBond() { if (sense == null) { return Observable.error(createNoDeviceException()); } // Intentionally not serialized on #queue return sense.removeBond() - .map(Functions.createMapperToVoid()); + .map(Func.justValue(this)); } public boolean isConnected() { @@ -172,12 +172,13 @@ public int getBondStatus() { //region Commands @CheckResult - public Observable runLedAnimation(@NonNull SenseLedAnimation animationType) { + public Observable runLedAnimation(@NonNull SenseLedAnimation animationType) { if (sense == null) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.runLedAnimation(animationType), queue); + return Rx.serialize(sense.runLedAnimation(animationType) + .map(Func.justValue(this)), queue); } @CheckResult @@ -210,48 +211,53 @@ public Observable sendWifiCredentials(@NonNull String } @CheckResult - public Observable linkAccount(@NonNull String accessToken) { + public Observable linkAccount(@NonNull String accessToken) { if (sense == null) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.linkAccount(accessToken), queue); + return Rx.serialize(sense.linkAccount(accessToken) + .map(Func.justValue(this)), queue); } @CheckResult - public Observable linkPill(@NonNull String accessToken) { + public Observable linkPill(@NonNull String accessToken) { if (sense == null) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.pairPill(accessToken), queue); + return Rx.serialize(sense.pairPill(accessToken) + .map(Func.justValue(this)), queue); } @CheckResult - public Observable pushData() { + public Observable pushData() { if (sense == null) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.pushData(), queue); + return Rx.serialize(sense.pushData() + .map(Func.justValue(this)), queue); } @CheckResult - public Observable putIntoPairingMode() { + public Observable putIntoPairingMode() { if (sense == null) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.putIntoPairingMode(), queue); + return Rx.serialize(sense.putIntoPairingMode() + .map(Func.justValue(this)), queue); } @CheckResult - public Observable factoryReset() { + public Observable factoryReset() { if (sense == null) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.factoryReset(), queue); + return Rx.serialize(sense.factoryReset() + .map(Func.justValue(this)), queue); } //endregion diff --git a/ble/src/main/java/is/hello/commonsense/util/Functions.java b/ble/src/main/java/is/hello/commonsense/util/Func.java similarity index 70% rename from ble/src/main/java/is/hello/commonsense/util/Functions.java rename to ble/src/main/java/is/hello/commonsense/util/Func.java index 73077aa..ba09ab4 100644 --- a/ble/src/main/java/is/hello/commonsense/util/Functions.java +++ b/ble/src/main/java/is/hello/commonsense/util/Func.java @@ -2,8 +2,8 @@ import rx.functions.Func1; -public class Functions { - public static Func1 createMapperToValue(final U value) { +public class Func { + public static Func1 justValue(final U value) { return new Func1() { @Override public U call(T ignored) { @@ -12,7 +12,7 @@ public U call(T ignored) { }; } - public static Func1 createMapperToVoid() { + public static Func1 justVoid() { return new Func1() { @Override public Void call(T ignored) { From e39d799e862e419cd6ec395046d20eac1fd89e88 Mon Sep 17 00:00:00 2001 From: km Date: Thu, 25 Feb 2016 13:30:26 -0800 Subject: [PATCH 12/28] convenience method for checking sense connectivity status; more tests --- .../service/SenseServiceConnection.java | 15 +- .../service/SenseServiceConnectionTests.java | 147 ++++++++++++++++++ .../service/SenseServiceTests.java | 20 --- 3 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java index 51e9430..e899812 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java @@ -92,13 +92,6 @@ public void unregisterConsumer(@NonNull Listener listener) { listeners.remove(listener); } - /** - * Unregisters all consumers from the helper. Not safe to call within {@link Listener} callbacks. - */ - public void removeAllConsumers() { - listeners.clear(); - } - /** * @return The {@link SenseService} if it's currently bound; {@code null} otherwise. */ @@ -134,6 +127,14 @@ public void onSenseServiceDisconnected() { } } + /** + * Indicates whether or not the {@link SenseService} is currently + * available, and connected to a remote Sense peripheral. + */ + public boolean isConnectedToSense() { + return (senseService != null && senseService.isConnected()); + } + //endregion diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java new file mode 100644 index 0000000..e7ae7ef --- /dev/null +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java @@ -0,0 +1,147 @@ +package is.hello.commonsense.service; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.robolectric.shadows.ShadowApplication; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import is.hello.commonsense.CommonSenseTestCase; +import is.hello.commonsense.bluetooth.SensePeripheral; +import rx.Observer; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SenseServiceConnectionTests extends CommonSenseTestCase { + private SenseService service; + + //region Lifecycle + + @Before + public void setUp() throws Exception { + final Context context = getContext(); + final ComponentName componentName = new ComponentName(context, SenseService.class); + this.service = new SenseService(); + service.onCreate(); + + final IBinder binder = service.onBind(new Intent(context, SenseService.class)); + ShadowApplication.getInstance().setComponentNameAndServiceForBindService(componentName, + binder); + } + + @After + public void tearDown() throws Exception { + service.onDestroy(); + } + + //endregion + + + @Test + public void lifecycle() { + final SenseServiceConnection.Listener listener = mock(SenseServiceConnection.Listener.class); + final SenseServiceConnection connection = new SenseServiceConnection(getContext()); + connection.registerConsumer(listener); + connection.create(); + + assertThat(connection.getSenseService(), is(notNullValue())); + //noinspection ConstantConditions + verify(listener).onSenseServiceConnected(connection.getSenseService()); + + connection.destroy(); + assertThat(connection.getSenseService(), is(nullValue())); + verify(listener).onSenseServiceDisconnected(); + } + + @Test + public void senseServiceCold() { + final SenseServiceConnection connection = new SenseServiceConnection(getContext()); + assertThat(connection.getSenseService(), is(nullValue())); + + final AtomicBoolean completed = new AtomicBoolean(false); + final AtomicReference service = new AtomicReference<>(); + connection.senseService().subscribe(new Observer() { + @Override + public void onCompleted() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + fail(); + } + + @Override + public void onNext(SenseService senseService) { + service.set(senseService); + } + }); + + connection.create(); + + assertThat(completed.get(), is(true)); + assertThat(service, is(notNullValue())); + } + + @Test + public void senseServiceWarm() { + final SenseServiceConnection connection = new SenseServiceConnection(getContext()); + connection.create(); + assertThat(connection.getSenseService(), is(notNullValue())); + + final AtomicBoolean completed = new AtomicBoolean(false); + final AtomicReference service = new AtomicReference<>(); + connection.senseService().subscribe(new Observer() { + @Override + public void onCompleted() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + fail(); + } + + @Override + public void onNext(SenseService senseService) { + service.set(senseService); + } + }); + + assertThat(completed.get(), is(true)); + assertThat(service, is(notNullValue())); + } + + @Test + public void isConnectedToSense() { + final SenseServiceConnection connection = new SenseServiceConnection(getContext()); + assertThat(connection.isConnectedToSense(), is(false)); + + connection.create(); + assertThat(connection.getSenseService(), is(notNullValue())); + assertThat(connection.isConnectedToSense(), is(false)); + + final SensePeripheral fakePeripheral = mock(SensePeripheral.class); + doReturn(true).when(fakePeripheral).isConnected(); + //noinspection ConstantConditions + connection.getSenseService().sense = fakePeripheral; + + assertThat(connection.isConnectedToSense(), is(true)); + + connection.destroy(); + } +} diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index b95f6c5..1062020 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -30,12 +30,9 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; public class SenseServiceTests extends CommonSenseTestCase { private SenseService service; @@ -61,23 +58,6 @@ public void tearDown() throws Exception { //endregion - - @Test - public void serviceConnection() { - final SenseServiceConnection.Listener listener = mock(SenseServiceConnection.Listener.class); - final SenseServiceConnection helper = new SenseServiceConnection(getContext()); - helper.registerConsumer(listener); - helper.create(); - - assertThat(helper.getSenseService(), is(notNullValue())); - //noinspection ConstantConditions - verify(listener).onSenseServiceConnected(helper.getSenseService()); - - helper.destroy(); - assertThat(helper.getSenseService(), is(nullValue())); - verify(listener).onSenseServiceDisconnected(); - } - @Test public void prepareForScan() { final AdvertisingData withoutDeviceId = new AdvertisingDataBuilder() From 30db7fa81911356e37aeb7c71e78ad68e06a534a Mon Sep 17 00:00:00 2001 From: km Date: Thu, 25 Feb 2016 13:42:30 -0800 Subject: [PATCH 13/28] clean up SenseService command APIs --- .../commonsense/service/SenseService.java | 49 +++++++++++++------ .../service/SenseServiceTests.java | 29 +++++++++-- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 1b75377..cd391ba 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -25,6 +25,7 @@ import is.hello.buruberi.util.SerialQueue; import is.hello.commonsense.bluetooth.SenseIdentifiers; import is.hello.commonsense.bluetooth.SensePeripheral; +import is.hello.commonsense.bluetooth.SensePeripheral.CountryCode; import is.hello.commonsense.bluetooth.model.SenseConnectToWiFiUpdate; import is.hello.commonsense.bluetooth.model.SenseLedAnimation; import is.hello.commonsense.bluetooth.model.SenseNetworkStatus; @@ -109,15 +110,19 @@ private void onPeripheralDisconnected() { queue.cancelPending(createNoDeviceException()); } - public static void prepareForScan(@NonNull PeripheralCriteria criteria, - @Nullable String deviceId) { + public static PeripheralCriteria createSenseCriteria() { + final PeripheralCriteria criteria = new PeripheralCriteria(); criteria.addExactMatchPredicate(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT); - if (deviceId != null) { - criteria.setLimit(1); - criteria.addStartsWithPredicate(AdvertisingData.TYPE_SERVICE_DATA, - SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + deviceId); - } + return criteria; + } + + public static PeripheralCriteria createSenseCriteria(@NonNull String deviceId) { + final PeripheralCriteria criteria = createSenseCriteria(); + criteria.setLimit(1); + criteria.addStartsWithPredicate(AdvertisingData.TYPE_SERVICE_DATA, + SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + deviceId); + return criteria; } @CheckResult @@ -158,14 +163,6 @@ public boolean isConnected() { return (sense != null && sense.isConnected()); } - public int getBondStatus() { - if (sense == null) { - return GattPeripheral.BOND_NONE; - } else { - return sense.getBondStatus(); - } - } - //endregion @@ -182,7 +179,27 @@ public Observable runLedAnimation(@NonNull SenseLedAnimation anima } @CheckResult - public Observable> scanForWifiNetworks(@Nullable SensePeripheral.CountryCode countryCode) { + public Observable trippyLEDs() { + return runLedAnimation(SenseLedAnimation.TRIPPY); + } + + @CheckResult + public Observable busyLEDs() { + return runLedAnimation(SenseLedAnimation.BUSY); + } + + @CheckResult + public Observable fadeOutLEDs() { + return runLedAnimation(SenseLedAnimation.FADE_OUT); + } + + @CheckResult + public Observable stopLEDs() { + return runLedAnimation(SenseLedAnimation.STOP); + } + + @CheckResult + public Observable> scanForWifiNetworks(@Nullable CountryCode countryCode) { if (sense == null) { return Observable.error(createNoDeviceException()); } diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index 1062020..75a1842 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -59,14 +59,13 @@ public void tearDown() throws Exception { //endregion @Test - public void prepareForScan() { + public void createSenseCriteria() { final AdvertisingData withoutDeviceId = new AdvertisingDataBuilder() .add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT) .build(); - final PeripheralCriteria criteriaWithoutDeviceId = new PeripheralCriteria(); - SenseService.prepareForScan(criteriaWithoutDeviceId, null); + final PeripheralCriteria criteriaWithoutDeviceId = SenseService.createSenseCriteria(); assertThat(criteriaWithoutDeviceId.matches(withoutDeviceId), is(true)); @@ -77,8 +76,7 @@ public void prepareForScan() { SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + Mocks.DEVICE_ID) .build(); - final PeripheralCriteria criteriaWithDeviceId = new PeripheralCriteria(); - SenseService.prepareForScan(criteriaWithDeviceId, Mocks.DEVICE_ID); + final PeripheralCriteria criteriaWithDeviceId = SenseService.createSenseCriteria(Mocks.DEVICE_ID); assertThat(criteriaWithDeviceId.limit, is(equalTo(1))); assertThat(criteriaWithDeviceId.matches(withDeviceId), is(true)); } @@ -145,6 +143,27 @@ public void runLedAnimationRequiresDevice() { Sync.last(service.runLedAnimation(SenseLedAnimation.BUSY)); } + @Test(expected = ConnectionStateException.class) + public void trippyLEDs() { + Sync.last(service.trippyLEDs()); + } + + @Test(expected = ConnectionStateException.class) + public void busyLEDs() { + Sync.last(service.busyLEDs()); + } + + @Test(expected = ConnectionStateException.class) + public void fadeOutLEDs() { + Sync.last(service.fadeOutLEDs()); + } + + @Test(expected = ConnectionStateException.class) + public void stopLEDs() { + Sync.last(service.stopLEDs()); + } + + @Test(expected = ConnectionStateException.class) public void scanForWifiNetworksRequiresDevice() { Sync.last(service.scanForWifiNetworks(null)); From d1556b841ac7d55b53b6502dcbd51bfed6b33ad3 Mon Sep 17 00:00:00 2001 From: km Date: Thu, 25 Feb 2016 16:31:52 -0800 Subject: [PATCH 14/28] remove useless #removeBond() method; add #getDeviceId method --- .../commonsense/service/SenseService.java | 16 +++++----------- .../commonsense/service/SenseServiceTests.java | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index cd391ba..0abda75 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -148,21 +148,15 @@ public Observable disconnect() { .map(Func.justValue(this)); } - @CheckResult - public Observable removeBond() { - if (sense == null) { - return Observable.error(createNoDeviceException()); - } - - // Intentionally not serialized on #queue - return sense.removeBond() - .map(Func.justValue(this)); - } - public boolean isConnected() { return (sense != null && sense.isConnected()); } + @Nullable + public String getDeviceId() { + return sense != null ? sense.getDeviceId() : null; + } + //endregion diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index 75a1842..0ce0ef6 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -33,6 +33,8 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; public class SenseServiceTests extends CommonSenseTestCase { private SenseService service; @@ -58,6 +60,17 @@ public void tearDown() throws Exception { //endregion + @Test + public void getDeviceId() { + assertThat(service.getDeviceId(), is(nullValue())); + + final SensePeripheral peripheral = mock(SensePeripheral.class); + doReturn(Mocks.DEVICE_ID).when(peripheral).getDeviceId(); + service.sense = peripheral; + + assertThat(service.getDeviceId(), is(equalTo(Mocks.DEVICE_ID))); + } + @Test public void createSenseCriteria() { final AdvertisingData withoutDeviceId = new AdvertisingDataBuilder() @@ -133,11 +146,6 @@ public void disconnectWithNoDevice() { assertThat(Sync.last(service.disconnect()), is(equalTo(null))); } - @Test(expected = ConnectionStateException.class) - public void removeBondRequiresDevice() { - Sync.last(service.removeBond()); - } - @Test(expected = ConnectionStateException.class) public void runLedAnimationRequiresDevice() { Sync.last(service.runLedAnimation(SenseLedAnimation.BUSY)); From bcd7b4436c0b5dccb548f58b587f5b77b6e8af86 Mon Sep 17 00:00:00 2001 From: km Date: Mon, 29 Feb 2016 11:03:13 -0800 Subject: [PATCH 15/28] add missing method to SenseService; add better interface for connection --- .../commonsense/service/SenseService.java | 12 ++++- .../service/SenseServiceConnection.java | 20 +++++++++ .../service/SenseServiceConnectionTests.java | 45 +++++++++++++++++++ .../service/SenseServiceTests.java | 9 +++- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 0abda75..2461f64 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -252,7 +252,7 @@ public Observable pushData() { } @CheckResult - public Observable putIntoPairingMode() { + public Observable enablePairingMode() { if (sense == null) { return Observable.error(createNoDeviceException()); } @@ -261,6 +261,16 @@ public Observable putIntoPairingMode() { .map(Func.justValue(this)), queue); } + @CheckResult + public Observable disablePairingMode() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return Rx.serialize(sense.putIntoNormalMode() + .map(Func.justValue(this)), queue); + } + @CheckResult public Observable factoryReset() { if (sense == null) { diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java index e899812..f17d9b8 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java @@ -12,6 +12,7 @@ import java.util.List; import rx.Observable; +import rx.functions.Func1; import rx.subjects.AsyncSubject; /** @@ -127,6 +128,25 @@ public void onSenseServiceDisconnected() { } } + /** + * Apply a functor producing an operation {@code Observable} + * to the {@link SenseService} this connection is bound to. + *

+ * This method is more efficient than just calling {@code #flatMap(Func1)} + * on the {@code Observable} returned by {@link #senseService()}. + * @param The type of value produced by the returned {@code Observable}. + * @param f Applied immediately if the {@code SenseService} is available; otherwise applied + * once the {@code SenseService} is connected. + * @return An operation observable. + */ + public Observable perform(@NonNull Func1> f) { + if (senseService != null) { + return f.call(senseService); + } else { + return senseService().flatMap(f); + } + } + /** * Indicates whether or not the {@link SenseService} is currently * available, and connected to a remote Sense peripheral. diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java index e7ae7ef..6bf8768 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java @@ -15,7 +15,9 @@ import is.hello.commonsense.CommonSenseTestCase; import is.hello.commonsense.bluetooth.SensePeripheral; +import rx.Observable; import rx.Observer; +import rx.functions.Func1; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; @@ -24,6 +26,8 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; public class SenseServiceConnectionTests extends CommonSenseTestCase { @@ -126,6 +130,47 @@ public void onNext(SenseService senseService) { assertThat(service, is(notNullValue())); } + @Test + public void performCold() { + final SenseServiceConnection connection = spy(new SenseServiceConnection(getContext())); + assertThat(connection.getSenseService(), is(nullValue())); + + final AtomicBoolean functorCalled = new AtomicBoolean(false); + connection.perform(new Func1>() { + @Override + public Observable call(SenseService service) { + functorCalled.set(true); + return Observable.just(service); + } + }).subscribe(); + + connection.create(); + + verify(connection).senseService(); + assertThat(functorCalled.get(), is(true)); + } + + @Test + public void performWarm() { + final SenseServiceConnection connection = spy(new SenseServiceConnection(getContext())); + connection.create(); + assertThat(connection.getSenseService(), is(notNullValue())); + + final AtomicBoolean functorCalled = new AtomicBoolean(false); + connection.perform(new Func1>() { + @Override + public Observable call(SenseService service) { + functorCalled.set(true); + return Observable.just(service); + } + }).subscribe(); + + connection.create(); + + verify(connection, never()).senseService(); + assertThat(functorCalled.get(), is(true)); + } + @Test public void isConnectedToSense() { final SenseServiceConnection connection = new SenseServiceConnection(getContext()); diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index 0ce0ef6..254faee 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -205,8 +205,13 @@ public void pushDataRequiresDevice() { } @Test(expected = ConnectionStateException.class) - public void putIntoPairingModeRequiresDevice() { - Sync.last(service.putIntoPairingMode()); + public void enablePairingModeRequiresDevice() { + Sync.last(service.enablePairingMode()); + } + + @Test(expected = ConnectionStateException.class) + public void disablePairingModeRequiresDevice() { + Sync.last(service.disablePairingMode()); } @Test(expected = ConnectionStateException.class) From b86cfdc692554e9a5d1f0475100b2cf1ad2f181c Mon Sep 17 00:00:00 2001 From: km Date: Mon, 29 Feb 2016 11:05:18 -0800 Subject: [PATCH 16/28] remove unused getter for stronger encapsulation guarantees --- .../service/SenseServiceConnection.java | 11 ++--------- .../service/SenseServiceConnectionTests.java | 18 +++++++++--------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java index f17d9b8..3505216 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java @@ -7,6 +7,7 @@ import android.os.IBinder; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import java.util.ArrayList; import java.util.List; @@ -21,7 +22,7 @@ public class SenseServiceConnection implements ServiceConnection { private final Context context; private final List listeners = new ArrayList<>(); - private @Nullable SenseService senseService; + @VisibleForTesting @Nullable SenseService senseService; //region Lifecycle @@ -93,14 +94,6 @@ public void unregisterConsumer(@NonNull Listener listener) { listeners.remove(listener); } - /** - * @return The {@link SenseService} if it's currently bound; {@code null} otherwise. - */ - @Nullable - public SenseService getSenseService() { - return senseService; - } - /** * Creates an {@code Observable} that will produce the * {@link SenseService} as soon as it becomes available. diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java index 6bf8768..6514cfb 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java @@ -62,19 +62,19 @@ public void lifecycle() { connection.registerConsumer(listener); connection.create(); - assertThat(connection.getSenseService(), is(notNullValue())); + assertThat(connection.senseService, is(notNullValue())); //noinspection ConstantConditions - verify(listener).onSenseServiceConnected(connection.getSenseService()); + verify(listener).onSenseServiceConnected(connection.senseService); connection.destroy(); - assertThat(connection.getSenseService(), is(nullValue())); + assertThat(connection.senseService, is(nullValue())); verify(listener).onSenseServiceDisconnected(); } @Test public void senseServiceCold() { final SenseServiceConnection connection = new SenseServiceConnection(getContext()); - assertThat(connection.getSenseService(), is(nullValue())); + assertThat(connection.senseService, is(nullValue())); final AtomicBoolean completed = new AtomicBoolean(false); final AtomicReference service = new AtomicReference<>(); @@ -105,7 +105,7 @@ public void onNext(SenseService senseService) { public void senseServiceWarm() { final SenseServiceConnection connection = new SenseServiceConnection(getContext()); connection.create(); - assertThat(connection.getSenseService(), is(notNullValue())); + assertThat(connection.senseService, is(notNullValue())); final AtomicBoolean completed = new AtomicBoolean(false); final AtomicReference service = new AtomicReference<>(); @@ -133,7 +133,7 @@ public void onNext(SenseService senseService) { @Test public void performCold() { final SenseServiceConnection connection = spy(new SenseServiceConnection(getContext())); - assertThat(connection.getSenseService(), is(nullValue())); + assertThat(connection.senseService, is(nullValue())); final AtomicBoolean functorCalled = new AtomicBoolean(false); connection.perform(new Func1>() { @@ -154,7 +154,7 @@ public Observable call(SenseService service) { public void performWarm() { final SenseServiceConnection connection = spy(new SenseServiceConnection(getContext())); connection.create(); - assertThat(connection.getSenseService(), is(notNullValue())); + assertThat(connection.senseService, is(notNullValue())); final AtomicBoolean functorCalled = new AtomicBoolean(false); connection.perform(new Func1>() { @@ -177,13 +177,13 @@ public void isConnectedToSense() { assertThat(connection.isConnectedToSense(), is(false)); connection.create(); - assertThat(connection.getSenseService(), is(notNullValue())); + assertThat(connection.senseService, is(notNullValue())); assertThat(connection.isConnectedToSense(), is(false)); final SensePeripheral fakePeripheral = mock(SensePeripheral.class); doReturn(true).when(fakePeripheral).isConnected(); //noinspection ConstantConditions - connection.getSenseService().sense = fakePeripheral; + connection.senseService.sense = fakePeripheral; assertThat(connection.isConnectedToSense(), is(true)); From fade5bac72d46fa3ee53c497ca97e25ab3b9761d Mon Sep 17 00:00:00 2001 From: km Date: Mon, 29 Feb 2016 11:06:01 -0800 Subject: [PATCH 17/28] remove troublesome nullability annotations --- .../main/java/is/hello/commonsense/service/SenseService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 2461f64..b0f061c 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -222,7 +222,7 @@ public Observable sendWifiCredentials(@NonNull String } @CheckResult - public Observable linkAccount(@NonNull String accessToken) { + public Observable linkAccount(String accessToken) { if (sense == null) { return Observable.error(createNoDeviceException()); } @@ -232,7 +232,7 @@ public Observable linkAccount(@NonNull String accessToken) { } @CheckResult - public Observable linkPill(@NonNull String accessToken) { + public Observable linkPill(String accessToken) { if (sense == null) { return Observable.error(createNoDeviceException()); } From a219ab3925f4437b2af967f0a8ca57f8e90fc21a Mon Sep 17 00:00:00 2001 From: km Date: Mon, 29 Feb 2016 12:00:35 -0800 Subject: [PATCH 18/28] better management of connections in SenseService --- .../main/java/is/hello/commonsense/service/SenseService.java | 2 +- .../java/is/hello/commonsense/service/SenseServiceTests.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index b0f061c..2ecff64 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -127,7 +127,7 @@ public static PeripheralCriteria createSenseCriteria(@NonNull String deviceId) { @CheckResult public Observable connect(@NonNull GattPeripheral peripheral) { - if (this.sense != null) { + if (this.sense != null && sense.isConnected()) { return Observable.error(new IllegalStateException("Cannot connect to multiple Senses at once.")); } diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index 254faee..026a53e 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -35,6 +35,7 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; public class SenseServiceTests extends CommonSenseTestCase { private SenseService service; @@ -98,7 +99,8 @@ public void createSenseCriteria() { public void connectSingleDeviceOnly() { final BluetoothStack stack = Mocks.createBluetoothStack(); final GattPeripheral device = Mocks.createPeripheral(stack); - service.sense = new SensePeripheral(device); + service.sense = spy(new SensePeripheral(device)); + doReturn(true).when(service.sense).isConnected(); Sync.last(service.connect(device)); // should throw } From 9b0bc54916954d9e45e6142155df94df1277197c Mon Sep 17 00:00:00 2001 From: km Date: Mon, 29 Feb 2016 13:30:44 -0800 Subject: [PATCH 19/28] remove broken magic registration of BuruberiReportingProvider --- ble/build.gradle | 8 ++++---- .../is/hello/commonsense/bluetooth/SensePeripheral.java | 5 ----- .../test/java/is/hello/commonsense/util/ErrorsTests.java | 5 +++++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ble/build.gradle b/ble/build.gradle index 8240318..4dab892 100644 --- a/ble/build.gradle +++ b/ble/build.gradle @@ -1,7 +1,8 @@ + import java.text.SimpleDateFormat // Used for both the 'aar' file and publish. -def VERSION_NAME = "2016.2.24.1" // ... +def VERSION_NAME = "2016.2.29.1" // ... def PACKAGE = 'is.hello.commonsense' apply plugin: 'com.android.library' @@ -12,7 +13,6 @@ def generateVersionCode() { def formatter = new SimpleDateFormat("yyMMddHH") Integer.parseInt(formatter.format(now)) } - android { compileSdkVersion 23 buildToolsVersion "23.0.1" @@ -44,7 +44,7 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile "org.robolectric:robolectric:3.0" + testCompile 'org.robolectric:robolectric:3.0' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.hamcrest:hamcrest-library:1.3' @@ -90,7 +90,7 @@ publishing { resolveStrategy = DELEGATE_FIRST name 'android-commonsense' description 'Client classes for Sense.' - url 'https://github.com/hello/buruberi' + url 'https://github.com/hello/android-commonsense' scm { url 'https://github.com/hello/android-commonsense' connection 'scm:git:git@github.com:hello/android-commonsense.git' diff --git a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java index b8c5e72..74d4c96 100644 --- a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java +++ b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java @@ -28,7 +28,6 @@ import is.hello.buruberi.bluetooth.stacks.util.LoggerFacade; import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; import is.hello.buruberi.util.Operation; -import is.hello.commonsense.bluetooth.errors.BuruberiReportingProvider; import is.hello.commonsense.bluetooth.errors.SenseBusyError; import is.hello.commonsense.bluetooth.errors.SenseConnectWifiError; import is.hello.commonsense.bluetooth.errors.SenseNotFoundError; @@ -57,10 +56,6 @@ public class SensePeripheral { public static final String LOG_TAG = SensePeripheral.class.getSimpleName(); - static { - BuruberiReportingProvider.register(); - } - //region Versions /** diff --git a/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java b/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java index f0b981d..cb77aed 100644 --- a/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java +++ b/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java @@ -10,6 +10,7 @@ import is.hello.buruberi.bluetooth.errors.BuruberiException; import is.hello.commonsense.CommonSenseTestCase; import is.hello.commonsense.R; +import is.hello.commonsense.bluetooth.errors.BuruberiReportingProvider; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; @@ -20,6 +21,10 @@ import static org.junit.Assert.assertThat; public class ErrorsTests extends CommonSenseTestCase { + public ErrorsTests() { + BuruberiReportingProvider.register(); + } + @Test public void getType() throws Exception { assertEquals("java.lang.Throwable", Errors.getType(new Throwable())); From 9a4c8dd0b65c905d5b6fe0ec85471ce31fd131aa Mon Sep 17 00:00:00 2001 From: km Date: Mon, 29 Feb 2016 15:53:21 -0800 Subject: [PATCH 20/28] guard against connection race conditions after discover services --- .../bluetooth/SensePeripheral.java | 45 +++++++++++-------- .../bluetooth/SensePeripheralTests.java | 3 ++ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java index 74d4c96..91a1301 100644 --- a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java +++ b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java @@ -16,6 +16,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; +import is.hello.buruberi.bluetooth.errors.ConnectionStateException; import is.hello.buruberi.bluetooth.errors.LostConnectionException; import is.hello.buruberi.bluetooth.errors.OperationTimeoutException; import is.hello.buruberi.bluetooth.stacks.BluetoothStack; @@ -178,16 +179,22 @@ public Observable connect() { connectFlags = GattPeripheral.CONNECT_FLAG_TRANSPORT_LE; } final OperationTimeout timeout = createStackTimeout("Connect"); - final Func1 onDiscoveredServices = new Func1() { + final Func1> onDiscoveredServices = new Func1>() { @Override - public ConnectProgress call(GattService service) { - SensePeripheral.this.gattService = service; - SensePeripheral.this.commandCharacteristic = - gattService.getCharacteristic(SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); - SensePeripheral.this.responseCharacteristic = - gattService.getCharacteristic(SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); - responseCharacteristic.setPacketListener(packetListener); - return ConnectProgress.CONNECTED; + public Observable call(GattService service) { + // Guard against the device connection being dropped in the + // 3 second delay that happens after discovering services. + if (gattPeripheral.getConnectionStatus() == GattPeripheral.STATUS_CONNECTED) { + SensePeripheral.this.gattService = service; + SensePeripheral.this.commandCharacteristic = + gattService.getCharacteristic(SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND); + SensePeripheral.this.responseCharacteristic = + gattService.getCharacteristic(SenseIdentifiers.CHARACTERISTIC_PROTOBUF_COMMAND_RESPONSE); + responseCharacteristic.setPacketListener(packetListener); + return Observable.just(ConnectProgress.CONNECTED); + } else { + return Observable.error(new ConnectionStateException()); + } } }; @@ -198,18 +205,18 @@ public ConnectProgress call(GattService service) { // behavior in KitKat and Gingerbread, which cannot establish // bonds without an active connection. sequence = Observable.concat( - Observable.just(ConnectProgress.BONDING), - gattPeripheral.createBond().map(Func.justValue(ConnectProgress.CONNECTING)), - gattPeripheral.connect(connectFlags, timeout).map(Func.justValue(ConnectProgress.DISCOVERING_SERVICES)), - gattPeripheral.discoverService(SenseIdentifiers.SERVICE, timeout).map(onDiscoveredServices) - ); + Observable.just(ConnectProgress.BONDING), + gattPeripheral.createBond().map(Func.justValue(ConnectProgress.CONNECTING)), + gattPeripheral.connect(connectFlags, timeout).map(Func.justValue(ConnectProgress.DISCOVERING_SERVICES)), + gattPeripheral.discoverService(SenseIdentifiers.SERVICE, timeout).flatMap(onDiscoveredServices) + ); } else { sequence = Observable.concat( - Observable.just(ConnectProgress.CONNECTING), - gattPeripheral.connect(connectFlags, timeout).map(Func.justValue(ConnectProgress.BONDING)), - gattPeripheral.createBond().map(Func.justValue(ConnectProgress.DISCOVERING_SERVICES)), - gattPeripheral.discoverService(SenseIdentifiers.SERVICE, timeout).map(onDiscoveredServices) - ); + Observable.just(ConnectProgress.CONNECTING), + gattPeripheral.connect(connectFlags, timeout).map(Func.justValue(ConnectProgress.BONDING)), + gattPeripheral.createBond().map(Func.justValue(ConnectProgress.DISCOVERING_SERVICES)), + gattPeripheral.discoverService(SenseIdentifiers.SERVICE, timeout).flatMap(onDiscoveredServices) + ); } return sequence.subscribeOn(gattPeripheral.getStack().getScheduler()) diff --git a/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java index b062240..6c5d12f 100644 --- a/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java +++ b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java @@ -211,6 +211,9 @@ public void connectSucceeded() throws Exception { doReturn(Observable.just(device)) .when(device) .createBond(); + doReturn(GattPeripheral.STATUS_CONNECTED) + .when(device) + .getConnectionStatus(); final GattService service = mock(GattService.class); doReturn(mock(GattCharacteristic.class)) .when(service) From dbea3cae994d87faf4f8642f3c9a3024ffe6d03f Mon Sep 17 00:00:00 2001 From: km Date: Mon, 29 Feb 2016 16:55:14 -0800 Subject: [PATCH 21/28] expose observable serialization for fun and profit --- .../commonsense/service/SenseService.java | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 2ecff64..aa40f38 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -87,6 +87,15 @@ private static Exception createNoDeviceException() { return new ConnectionStateException("Not connected to Sense"); } + /** + * Binds an {@code Observable} to the {@code SenseService}'s internal job queue. The returned + * observable will act like observables returned by other methods in {@code SenseService} and + * only run when all work submitted before it has completed. + */ + public Observable serialize(@NonNull Observable observable) { + return Rx.serialize(observable, queue); + } + //endregion @@ -133,8 +142,7 @@ public Observable connect(@NonNull GattPeripheral peripheral) { this.sense = new SensePeripheral(peripheral); - // Intentionally not serialized on #queue - return sense.connect(); + return serialize(sense.connect()); } @CheckResult @@ -143,7 +151,8 @@ public Observable disconnect() { return Observable.just(null); } - // Intentionally not serialized on #queue + // Intentionally not serialized on #queue so that disconnect + // can happen as soon as possible relative to its call site. return sense.disconnect() .map(Func.justValue(this)); } @@ -168,8 +177,8 @@ public Observable runLedAnimation(@NonNull SenseLedAnimation anima return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.runLedAnimation(animationType) - .map(Func.justValue(this)), queue); + return sense.runLedAnimation(animationType) + .map(Func.justValue(this)); } @CheckResult @@ -198,7 +207,7 @@ public Observable> scanForWifiNetworks(@Nullable CountryCode return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.scanForWifiNetworks(countryCode), queue); + return sense.scanForWifiNetworks(countryCode); } @CheckResult @@ -207,7 +216,7 @@ public Observable currentWifiNetwork() { return Observable.error(createNoDeviceException()); } - return sense.getWifiNetwork(); + return serialize(sense.getWifiNetwork()); } @CheckResult @@ -218,7 +227,7 @@ public Observable sendWifiCredentials(@NonNull String return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.connectToWiFiNetwork(ssid, securityType, password), queue); + return sense.connectToWiFiNetwork(ssid, securityType, password); } @CheckResult @@ -227,8 +236,8 @@ public Observable linkAccount(String accessToken) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.linkAccount(accessToken) - .map(Func.justValue(this)), queue); + return sense.linkAccount(accessToken) + .map(Func.justValue(this)); } @CheckResult @@ -237,8 +246,8 @@ public Observable linkPill(String accessToken) { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.pairPill(accessToken) - .map(Func.justValue(this)), queue); + return sense.pairPill(accessToken) + .map(Func.justValue(this)); } @CheckResult @@ -247,8 +256,8 @@ public Observable pushData() { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.pushData() - .map(Func.justValue(this)), queue); + return sense.pushData() + .map(Func.justValue(this)); } @CheckResult @@ -257,8 +266,8 @@ public Observable enablePairingMode() { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.putIntoPairingMode() - .map(Func.justValue(this)), queue); + return sense.putIntoPairingMode() + .map(Func.justValue(this)); } @CheckResult @@ -267,8 +276,8 @@ public Observable disablePairingMode() { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.putIntoNormalMode() - .map(Func.justValue(this)), queue); + return sense.putIntoNormalMode() + .map(Func.justValue(this)); } @CheckResult @@ -277,8 +286,8 @@ public Observable factoryReset() { return Observable.error(createNoDeviceException()); } - return Rx.serialize(sense.factoryReset() - .map(Func.justValue(this)), queue); + return sense.factoryReset() + .map(Func.justValue(this)); } //endregion From b0c2e07a547f9abeba9acd96a53b0cc3f84b3075 Mon Sep 17 00:00:00 2001 From: km Date: Tue, 1 Mar 2016 16:33:47 -0800 Subject: [PATCH 22/28] add support for foregrounding the SenseService for better multi-tasking support --- .../commonsense/service/SenseService.java | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index aa40f38..755966e 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -1,5 +1,6 @@ package is.hello.commonsense.service; +import android.app.Notification; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; @@ -33,6 +34,7 @@ import is.hello.commonsense.util.ConnectProgress; import is.hello.commonsense.util.Func; import rx.Observable; +import rx.functions.Action0; public class SenseService extends Service { private static final String LOG_TAG = SenseService.class.getSimpleName(); @@ -40,6 +42,10 @@ public class SenseService extends Service { @VisibleForTesting final SerialQueue queue = new SerialQueue(); @VisibleForTesting @Nullable SensePeripheral sense; + private int notificationId = 0; + private @Nullable Notification notification; + private int foregroundCount = 0; + //region Service Lifecycle private final LocalBinder binder = new LocalBinder(); @@ -99,6 +105,67 @@ public Observable serialize(@NonNull Observable observable) { //endregion + //region Foregrounding + + private void incrementForeground() { + if (notification == null) { + throw new IllegalStateException("Cannot call incrementForeground() before setting a notification"); + } + + this.foregroundCount++; + + if (foregroundCount == 1) { + startForeground(notificationId, notification); + } + } + + private void decrementForeground() { + if (foregroundCount == 0) { + Log.w(LOG_TAG, "decrementForeground() called too many times"); + } + + this.foregroundCount--; + + if (foregroundCount == 0) { + stopForeground(true); + } + } + + /** + * Specifies the notification to display when the {@code SenseService} + * is connected to Sense, and has entered foreground mode. + *

+ * This method should be called before a connection is created. + * + * @param id The id of the notification in the notification manager. Cannot be 0. + * @param notification The notification to display. + */ + public void setForegroundNotification(int id, @Nullable Notification notification) { + if (id == 0 && notification != null) { + throw new IllegalArgumentException("id cannot be 0"); + } + + this.notificationId = id; + this.notification = notification; + + if (notification == null && foregroundCount > 0) { + stopForeground(true); + this.foregroundCount = 0; + } + } + + /** + * Indicates whether or not foregrounding is currently enabled for the service. + * This is contingent on the service having a notification bound to it. + * @return true if the service will run in the foreground when connected to a sense; false otherwise. + */ + public boolean isForegroundingEnabled() { + return (notificationId > 0 && notification != null); + } + + //endregion + + //region Managing Connectivity private final BroadcastReceiver peripheralDisconnected = new BroadcastReceiver() { @@ -117,6 +184,9 @@ public void onReceive(Context context, Intent intent) { private void onPeripheralDisconnected() { this.sense = null; queue.cancelPending(createNoDeviceException()); + if (isForegroundingEnabled()) { + decrementForeground(); + } } public static PeripheralCriteria createSenseCriteria() { @@ -142,7 +212,14 @@ public Observable connect(@NonNull GattPeripheral peripheral) { this.sense = new SensePeripheral(peripheral); - return serialize(sense.connect()); + return serialize(sense.connect()).doOnCompleted(new Action0() { + @Override + public void call() { + if (isForegroundingEnabled()) { + incrementForeground(); + } + } + }); } @CheckResult From 9066c4d0230af4f7441da8a6dbc05567c4e0d9d2 Mon Sep 17 00:00:00 2001 From: km Date: Wed, 2 Mar 2016 10:38:31 -0800 Subject: [PATCH 23/28] fix some bugs around foregrounding --- .../main/java/is/hello/commonsense/service/SenseService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 755966e..848ba9b 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -115,6 +115,7 @@ private void incrementForeground() { this.foregroundCount++; if (foregroundCount == 1) { + Log.d(LOG_TAG, "Starting foregrounding"); startForeground(notificationId, notification); } } @@ -122,11 +123,13 @@ private void incrementForeground() { private void decrementForeground() { if (foregroundCount == 0) { Log.w(LOG_TAG, "decrementForeground() called too many times"); + return; } this.foregroundCount--; if (foregroundCount == 0) { + Log.d(LOG_TAG, "Stopping foregrounding"); stopForeground(true); } } @@ -160,7 +163,7 @@ public void setForegroundNotification(int id, @Nullable Notification notificatio * @return true if the service will run in the foreground when connected to a sense; false otherwise. */ public boolean isForegroundingEnabled() { - return (notificationId > 0 && notification != null); + return (notificationId != 0 && notification != null); } //endregion From 6d20ba8fc0ef0dfb271726c9ed55a24be9bfe204 Mon Sep 17 00:00:00 2001 From: km Date: Wed, 2 Mar 2016 14:29:41 -0800 Subject: [PATCH 24/28] deprecations; tests + code clean up; documentation --- .../bluetooth/SensePeripheral.java | 12 ++ .../commonsense/service/SenseService.java | 181 +++++++++++------- .../service/SenseServiceTests.java | 66 ++++++- 3 files changed, 183 insertions(+), 76 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java index 91a1301..00ac616 100644 --- a/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java +++ b/ble/src/main/java/is/hello/commonsense/bluetooth/SensePeripheral.java @@ -41,6 +41,7 @@ import is.hello.commonsense.bluetooth.model.SenseNetworkStatus; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_endpoint; +import is.hello.commonsense.service.SenseService; import is.hello.commonsense.util.ConnectProgress; import is.hello.commonsense.util.Func; import rx.Observable; @@ -54,6 +55,9 @@ import static is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.MorpheusCommand.CommandType; import static is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_connection_state; +/** + * Prefer {@link SenseService} for all new code. + */ public class SensePeripheral { public static final String LOG_TAG = SensePeripheral.class.getSimpleName(); @@ -108,7 +112,11 @@ public enum CountryCode { //region Lifecycle + /** + * @deprecated Use {@link SenseService} with your own peripheral discovery code. + */ @CheckResult + @Deprecated public static Observable> discover(@NonNull BluetoothStack bluetoothStack, @NonNull PeripheralCriteria criteria) { criteria.addExactMatchPredicate(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, @@ -121,7 +129,11 @@ public List call(List peripherals) { }); } + /** + * @deprecated Use {@link SenseService} with your own peripheral discovery code. + */ @CheckResult + @Deprecated public static Observable rediscover(@NonNull BluetoothStack bluetoothStack, @NonNull String deviceId, boolean includeHighPowerPreScan) { diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 848ba9b..fbb39fd 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -36,15 +36,23 @@ import rx.Observable; import rx.functions.Action0; +/** + * Encapsulates connection and command management for communicating with a Sense over Bluetooth + * Low Energy. The {@code SenseService} class replaces the {@link SensePeripheral} class for all + * new code. It provides stronger guarantees around command serialization, and host process + * lifecycle management. + *

+ * {@code SenseService} is automatically exported when you include the CommonSense library through + * maven/gradle. To communicate with {@code SenseService}, use {@link SenseServiceConnection}. + */ public class SenseService extends Service { private static final String LOG_TAG = SenseService.class.getSimpleName(); @VisibleForTesting final SerialQueue queue = new SerialQueue(); @VisibleForTesting @Nullable SensePeripheral sense; - private int notificationId = 0; - private @Nullable Notification notification; - private int foregroundCount = 0; + private ForegroundNotificationProvider notificationProvider; + @VisibleForTesting boolean foregroundEnabled = false; //region Service Lifecycle @@ -107,65 +115,45 @@ public Observable serialize(@NonNull Observable observable) { //region Foregrounding - private void incrementForeground() { - if (notification == null) { - throw new IllegalStateException("Cannot call incrementForeground() before setting a notification"); - } - - this.foregroundCount++; - - if (foregroundCount == 1) { - Log.d(LOG_TAG, "Starting foregrounding"); - startForeground(notificationId, notification); - } - } - - private void decrementForeground() { - if (foregroundCount == 0) { - Log.w(LOG_TAG, "decrementForeground() called too many times"); - return; - } + /** + * Controls the state of foregrounding for the service. This method is idempotent. + * @param enabled Whether or not foregrounding is enabled. + * @throws IllegalStateException if {@code enabled} is true, and no notification provider is set. + * @see #setForegroundNotificationProvider(ForegroundNotificationProvider) + */ + @VisibleForTesting void setForegroundEnabled(boolean enabled) { + if (enabled != this.foregroundEnabled) { + this.foregroundEnabled = enabled; - this.foregroundCount--; + if (enabled) { + if (notificationProvider == null) { + throw new IllegalStateException("Cannot enable foregrounding without a notification provider"); + } - if (foregroundCount == 0) { - Log.d(LOG_TAG, "Stopping foregrounding"); - stopForeground(true); + Log.d(LOG_TAG, "Foregrounding enabled"); + startForeground(notificationProvider.getId(), + notificationProvider.getNotification()); + } else { + Log.d(LOG_TAG, "Foregrounding disabled"); + stopForeground(true); + } } } /** - * Specifies the notification to display when the {@code SenseService} - * is connected to Sense, and has entered foreground mode. - *

- * This method should be called before a connection is created. + * Sets the foreground notification provider the service should use to establish foreground + * status when it has an active connection with a remote Sense peripheral. * - * @param id The id of the notification in the notification manager. Cannot be 0. - * @param notification The notification to display. + * @param provider The new provider. If {@code null}, foreground status will be discontinued. */ - public void setForegroundNotification(int id, @Nullable Notification notification) { - if (id == 0 && notification != null) { - throw new IllegalArgumentException("id cannot be 0"); - } - - this.notificationId = id; - this.notification = notification; + public void setForegroundNotificationProvider(@Nullable ForegroundNotificationProvider provider) { + this.notificationProvider = provider; - if (notification == null && foregroundCount > 0) { - stopForeground(true); - this.foregroundCount = 0; + if (provider == null && foregroundEnabled) { + setForegroundEnabled(false); } } - /** - * Indicates whether or not foregrounding is currently enabled for the service. - * This is contingent on the service having a notification bound to it. - * @return true if the service will run in the foreground when connected to a sense; false otherwise. - */ - public boolean isForegroundingEnabled() { - return (notificationId != 0 && notification != null); - } - //endregion @@ -184,14 +172,16 @@ public void onReceive(Context context, Intent intent) { } }; - private void onPeripheralDisconnected() { + @VisibleForTesting void onPeripheralDisconnected() { this.sense = null; queue.cancelPending(createNoDeviceException()); - if (isForegroundingEnabled()) { - decrementForeground(); - } + setForegroundEnabled(false); } + /** + * Creates a {@code PeripheralCriteria} object configured to match zero or more Sense peripherals. + * @return A new {@code PeripheralCriteria} object. + */ public static PeripheralCriteria createSenseCriteria() { final PeripheralCriteria criteria = new PeripheralCriteria(); criteria.addExactMatchPredicate(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, @@ -199,6 +189,12 @@ public static PeripheralCriteria createSenseCriteria() { return criteria; } + /** + * Creates a {@code PeripheralCriteria} object configured + * to match a single Sense peripheral with a given device id. + * @param deviceId The device id to match. + * @return A new {@code PeripheralCriteria} object. + */ public static PeripheralCriteria createSenseCriteria(@NonNull String deviceId) { final PeripheralCriteria criteria = createSenseCriteria(); criteria.setLimit(1); @@ -207,6 +203,12 @@ public static PeripheralCriteria createSenseCriteria(@NonNull String deviceId) { return criteria; } + /** + * Attempts to connect to a Sense {@code GattPeripheral} object, implicitly enabling + * service foregrounding if possible after a connection is successfully established. + * @param peripheral The Sense to connect to. + * @return An {@code Observable} representing the connection attempt operation. + */ @CheckResult public Observable connect(@NonNull GattPeripheral peripheral) { if (this.sense != null && sense.isConnected()) { @@ -218,17 +220,26 @@ public Observable connect(@NonNull GattPeripheral peripheral) { return serialize(sense.connect()).doOnCompleted(new Action0() { @Override public void call() { - if (isForegroundingEnabled()) { - incrementForeground(); + if (notificationProvider != null) { + setForegroundEnabled(true); } } }); } + /** + * Disconnects from the Sense {@code GattPeripheral} the service is connected to. + * The operation {@code Observable} returned by this method is not serialized like the other + * observables returned by {@link SenseService}, the disconnect is guaranteed to be issued + * immediately upon subscription. + *

+ * This method does nothing if there is no active connection. + * @return A disconnect operation {@code Observable}. + */ @CheckResult public Observable disconnect() { if (sense == null) { - return Observable.just(null); + return Observable.just(this); } // Intentionally not serialized on #queue so that disconnect @@ -237,10 +248,18 @@ public Observable disconnect() { .map(Func.justValue(this)); } + /** + * Indicates whether or not the service is connected to a Sense peripheral. + * @return true if the service is connected; false otherwise. + */ public boolean isConnected() { return (sense != null && sense.isConnected()); } + /** + * Extracts the Sense device id for the currently connected peripheral. + * @return The device id for the Sense if one is connected; null otherwise. + */ @Nullable public String getDeviceId() { return sense != null ? sense.getDeviceId() : null; @@ -252,33 +271,43 @@ public String getDeviceId() { //region Commands @CheckResult - public Observable runLedAnimation(@NonNull SenseLedAnimation animationType) { + public Observable trippyLEDs() { if (sense == null) { return Observable.error(createNoDeviceException()); } - return sense.runLedAnimation(animationType) + return sense.runLedAnimation(SenseLedAnimation.TRIPPY) .map(Func.justValue(this)); } - @CheckResult - public Observable trippyLEDs() { - return runLedAnimation(SenseLedAnimation.TRIPPY); - } - @CheckResult public Observable busyLEDs() { - return runLedAnimation(SenseLedAnimation.BUSY); + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.runLedAnimation(SenseLedAnimation.BUSY) + .map(Func.justValue(this)); } @CheckResult public Observable fadeOutLEDs() { - return runLedAnimation(SenseLedAnimation.FADE_OUT); + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.runLedAnimation(SenseLedAnimation.FADE_OUT) + .map(Func.justValue(this)); } @CheckResult public Observable stopLEDs() { - return runLedAnimation(SenseLedAnimation.STOP); + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return sense.runLedAnimation(SenseLedAnimation.STOP) + .map(Func.justValue(this)); } @CheckResult @@ -371,4 +400,22 @@ public Observable factoryReset() { } //endregion + + + /** + * Provides the notification used when the {@link SenseService} enters foreground mode. + */ + public interface ForegroundNotificationProvider { + /** + * Get the id to use for the notification. + * @return The id. Must not be {@code 0}. + */ + int getId(); + + /** + * Get the notification to display. Should have {@code PRIORITY_MIN}, and be marked as ongoing. + * @return The notification to display when in the service is in the foreground. + */ + @NonNull Notification getNotification(); + } } diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index 026a53e..26737b2 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -1,9 +1,12 @@ package is.hello.commonsense.service; +import android.app.Notification; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; import org.junit.After; @@ -24,8 +27,8 @@ import is.hello.commonsense.Mocks; import is.hello.commonsense.bluetooth.SenseIdentifiers; import is.hello.commonsense.bluetooth.SensePeripheral; -import is.hello.commonsense.bluetooth.model.SenseLedAnimation; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_endpoint.sec_type; +import is.hello.commonsense.util.ConnectProgress; import is.hello.commonsense.util.Sync; import static org.hamcrest.CoreMatchers.equalTo; @@ -33,10 +36,16 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +@SuppressWarnings("ResourceType") public class SenseServiceTests extends CommonSenseTestCase { private SenseService service; @@ -104,6 +113,43 @@ public void connectSingleDeviceOnly() { Sync.last(service.connect(device)); // should throw } + public void connectStartsForegrounding() { + final SenseService service = spy(this.service); + doNothing().when(service).startForeground(anyInt(), any(Notification.class)); + + service.setForegroundNotificationProvider(new SenseService.ForegroundNotificationProvider() { + @Override + public int getId() { + return 1; + } + + @NonNull + @Override + public Notification getNotification() { + return new NotificationCompat.Builder(getContext()).build(); + } + }); + + final BluetoothStack stack = Mocks.createBluetoothStack(); + final GattPeripheral device = Mocks.createPeripheral(stack); + doReturn(GattPeripheral.STATUS_CONNECTED).when(device).getConnectionStatus(); + assertThat(Sync.last(service.connect(device)), is(equalTo(ConnectProgress.CONNECTED))); + + verify(service).startForeground(anyInt(), any(Notification.class)); + verify(service).setForegroundEnabled(true); + } + + public void disconnectStopsForegrounding() { + final SenseService service = spy(this.service); + service.foregroundEnabled = true; + doNothing().when(service).stopForeground(anyBoolean()); + + service.onPeripheralDisconnected(); + + verify(service).stopForeground(anyBoolean()); + verify(service).setForegroundEnabled(false); + } + @Test public void peripheralDisconnected() { final BluetoothStack stack = Mocks.createBluetoothStack(); @@ -145,31 +191,33 @@ public void run() { @Test public void disconnectWithNoDevice() { - assertThat(Sync.last(service.disconnect()), is(equalTo(null))); + assertThat(Sync.last(service.disconnect()), is(equalTo(service))); } - @Test(expected = ConnectionStateException.class) - public void runLedAnimationRequiresDevice() { - Sync.last(service.runLedAnimation(SenseLedAnimation.BUSY)); + @Test + public void setForegroundNotificationProviderDisablesForegroundOnNull() { + service.foregroundEnabled = true; + service.setForegroundNotificationProvider(null); + assertThat(service.foregroundEnabled, is(false)); } @Test(expected = ConnectionStateException.class) - public void trippyLEDs() { + public void trippyLEDsRequiresDevice() { Sync.last(service.trippyLEDs()); } @Test(expected = ConnectionStateException.class) - public void busyLEDs() { + public void busyLEDsRequiresDevice() { Sync.last(service.busyLEDs()); } @Test(expected = ConnectionStateException.class) - public void fadeOutLEDs() { + public void fadeOutLEDsRequiresDevice() { Sync.last(service.fadeOutLEDs()); } @Test(expected = ConnectionStateException.class) - public void stopLEDs() { + public void stopLEDsRequiresDevice() { Sync.last(service.stopLEDs()); } From 4b5526ca2baa7d25ecc223bf068bf5683268ff37 Mon Sep 17 00:00:00 2001 From: km Date: Wed, 2 Mar 2016 14:44:07 -0800 Subject: [PATCH 25/28] switch to connected broadcast for foreground side-effects --- .../commonsense/service/SenseService.java | 31 ++++++++++--------- .../service/SenseServiceTests.java | 6 +--- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index fbb39fd..0a25915 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -34,7 +34,6 @@ import is.hello.commonsense.util.ConnectProgress; import is.hello.commonsense.util.Func; import rx.Observable; -import rx.functions.Action0; /** * Encapsulates connection and command management for communicating with a Sense over Bluetooth @@ -75,9 +74,11 @@ public IBinder onBind(Intent intent) { public void onCreate() { super.onCreate(); - final IntentFilter intentFilter = new IntentFilter(GattPeripheral.ACTION_DISCONNECTED); + final IntentFilter connectionIntentFilter = new IntentFilter(); + connectionIntentFilter.addAction(GattPeripheral.ACTION_CONNECTED); + connectionIntentFilter.addAction(GattPeripheral.ACTION_DISCONNECTED); LocalBroadcastManager.getInstance(this) - .registerReceiver(peripheralDisconnected, intentFilter); + .registerReceiver(connectionBroadcastReceiver, connectionIntentFilter); } @Override @@ -85,7 +86,7 @@ public void onDestroy() { super.onDestroy(); LocalBroadcastManager.getInstance(this) - .unregisterReceiver(peripheralDisconnected); + .unregisterReceiver(connectionBroadcastReceiver); if (sense != null && sense.isConnected()) { Log.w(LOG_TAG, "Service being destroyed with active connection"); @@ -159,19 +160,28 @@ public void setForegroundNotificationProvider(@Nullable ForegroundNotificationPr //region Managing Connectivity - private final BroadcastReceiver peripheralDisconnected = new BroadcastReceiver() { + private final BroadcastReceiver connectionBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (sense != null) { final String intentAddress = intent.getStringExtra(GattPeripheral.EXTRA_ADDRESS); final String senseAddress = sense.getAddress(); if (Objects.equals(intentAddress, senseAddress)) { - onPeripheralDisconnected(); + final String action = intent.getAction(); + if (action.equals(GattPeripheral.ACTION_CONNECTED)) { + onPeripheralConnected(); + } else if (action.equals(GattPeripheral.ACTION_DISCONNECTED)) { + onPeripheralDisconnected(); + } } } } }; + @VisibleForTesting void onPeripheralConnected() { + setForegroundEnabled(true); + } + @VisibleForTesting void onPeripheralDisconnected() { this.sense = null; queue.cancelPending(createNoDeviceException()); @@ -217,14 +227,7 @@ public Observable connect(@NonNull GattPeripheral peripheral) { this.sense = new SensePeripheral(peripheral); - return serialize(sense.connect()).doOnCompleted(new Action0() { - @Override - public void call() { - if (notificationProvider != null) { - setForegroundEnabled(true); - } - } - }); + return serialize(sense.connect()); } /** diff --git a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java index 26737b2..19c7ab7 100644 --- a/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -28,7 +28,6 @@ import is.hello.commonsense.bluetooth.SenseIdentifiers; import is.hello.commonsense.bluetooth.SensePeripheral; import is.hello.commonsense.bluetooth.model.protobuf.SenseCommandProtos.wifi_endpoint.sec_type; -import is.hello.commonsense.util.ConnectProgress; import is.hello.commonsense.util.Sync; import static org.hamcrest.CoreMatchers.equalTo; @@ -130,10 +129,7 @@ public Notification getNotification() { } }); - final BluetoothStack stack = Mocks.createBluetoothStack(); - final GattPeripheral device = Mocks.createPeripheral(stack); - doReturn(GattPeripheral.STATUS_CONNECTED).when(device).getConnectionStatus(); - assertThat(Sync.last(service.connect(device)), is(equalTo(ConnectProgress.CONNECTED))); + service.onPeripheralConnected(); verify(service).startForeground(anyInt(), any(Notification.class)); verify(service).setForegroundEnabled(true); From a6a2550473d1f3f7d5fd737e1f7cf86014450243 Mon Sep 17 00:00:00 2001 From: km Date: Thu, 3 Mar 2016 13:16:38 -0800 Subject: [PATCH 26/28] bump version --- ble/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ble/build.gradle b/ble/build.gradle index 4dab892..753eee4 100644 --- a/ble/build.gradle +++ b/ble/build.gradle @@ -2,7 +2,7 @@ import java.text.SimpleDateFormat // Used for both the 'aar' file and publish. -def VERSION_NAME = "2016.2.29.1" // ... +def VERSION_NAME = "2016.3.7.1" // ... def PACKAGE = 'is.hello.commonsense' apply plugin: 'com.android.library' From 57a736819024737f1bb9bb33de9f25c7fffce266 Mon Sep 17 00:00:00 2001 From: km Date: Fri, 4 Mar 2016 11:04:07 -0800 Subject: [PATCH 27/28] switch to snapshot deploy mode --- ble/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ble/build.gradle b/ble/build.gradle index c5eff58..b30cf4a 100644 --- a/ble/build.gradle +++ b/ble/build.gradle @@ -1,7 +1,7 @@ import java.text.SimpleDateFormat // Used for both the 'aar' file and publish. -def VERSION_NAME = "2016.3.7.1" // ... +def VERSION_NAME = "2016.3.7.1-SNAPSHOT" // ... def PACKAGE = 'is.hello.commonsense' apply plugin: 'com.android.library' From eabd2a27dc69e8a0bf6f5e1d35d29bae5010c0b3 Mon Sep 17 00:00:00 2001 From: km Date: Fri, 4 Mar 2016 14:51:42 -0800 Subject: [PATCH 28/28] add missing task serialization calls --- .../commonsense/service/SenseService.java | 100 +++++++++++++----- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/ble/src/main/java/is/hello/commonsense/service/SenseService.java b/ble/src/main/java/is/hello/commonsense/service/SenseService.java index 0a25915..dfb469e 100644 --- a/ble/src/main/java/is/hello/commonsense/service/SenseService.java +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -19,6 +19,7 @@ import java.util.Objects; import is.hello.buruberi.bluetooth.errors.ConnectionStateException; +import is.hello.buruberi.bluetooth.errors.OperationTimeoutException; import is.hello.buruberi.bluetooth.stacks.GattPeripheral; import is.hello.buruberi.bluetooth.stacks.util.AdvertisingData; import is.hello.buruberi.bluetooth.stacks.util.PeripheralCriteria; @@ -34,6 +35,9 @@ import is.hello.commonsense.util.ConnectProgress; import is.hello.commonsense.util.Func; import rx.Observable; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; /** * Encapsulates connection and command management for communicating with a Sense over Bluetooth @@ -102,6 +106,35 @@ private static Exception createNoDeviceException() { return new ConnectionStateException("Not connected to Sense"); } + private Action1 createCleanUpHandler() { + return new Action1() { + @Override + public void call(Throwable e) { + // There is basically no recovering from a timeout, it's best + // to just abandon the connection and get on with our lives. + if (e instanceof OperationTimeoutException) { + Log.d(LOG_TAG, "Dropping connection after timeout"); + disconnect().subscribe(new Subscriber() { + @Override + public void onCompleted() { + Log.d(LOG_TAG, "Connection dropped"); + } + + @Override + public void onError(Throwable e) { + Log.e(LOG_TAG, "Could not drop connection cleanly", e); + } + + @Override + public void onNext(SenseService service) { + // Do nothing. + } + }); + } + } + }; + } + /** * Binds an {@code Observable} to the {@code SenseService}'s internal job queue. The returned * observable will act like observables returned by other methods in {@code SenseService} and @@ -248,7 +281,13 @@ public Observable disconnect() { // Intentionally not serialized on #queue so that disconnect // can happen as soon as possible relative to its call site. return sense.disconnect() - .map(Func.justValue(this)); + .map(Func.justValue(this)) + .doOnCompleted(new Action0() { + @Override + public void call() { + queue.cancelPending(); + } + }); } /** @@ -279,8 +318,9 @@ public Observable trippyLEDs() { return Observable.error(createNoDeviceException()); } - return sense.runLedAnimation(SenseLedAnimation.TRIPPY) - .map(Func.justValue(this)); + return serialize(sense.runLedAnimation(SenseLedAnimation.TRIPPY) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -289,8 +329,9 @@ public Observable busyLEDs() { return Observable.error(createNoDeviceException()); } - return sense.runLedAnimation(SenseLedAnimation.BUSY) - .map(Func.justValue(this)); + return serialize(sense.runLedAnimation(SenseLedAnimation.BUSY) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -299,8 +340,9 @@ public Observable fadeOutLEDs() { return Observable.error(createNoDeviceException()); } - return sense.runLedAnimation(SenseLedAnimation.FADE_OUT) - .map(Func.justValue(this)); + return serialize(sense.runLedAnimation(SenseLedAnimation.FADE_OUT) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -309,8 +351,9 @@ public Observable stopLEDs() { return Observable.error(createNoDeviceException()); } - return sense.runLedAnimation(SenseLedAnimation.STOP) - .map(Func.justValue(this)); + return serialize(sense.runLedAnimation(SenseLedAnimation.STOP) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -319,7 +362,8 @@ public Observable> scanForWifiNetworks(@Nullable CountryCode return Observable.error(createNoDeviceException()); } - return sense.scanForWifiNetworks(countryCode); + return serialize(sense.scanForWifiNetworks(countryCode) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -328,7 +372,8 @@ public Observable currentWifiNetwork() { return Observable.error(createNoDeviceException()); } - return serialize(sense.getWifiNetwork()); + return serialize(sense.getWifiNetwork() + .doOnError(createCleanUpHandler())); } @CheckResult @@ -339,7 +384,8 @@ public Observable sendWifiCredentials(@NonNull String return Observable.error(createNoDeviceException()); } - return sense.connectToWiFiNetwork(ssid, securityType, password); + return serialize(sense.connectToWiFiNetwork(ssid, securityType, password) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -348,8 +394,9 @@ public Observable linkAccount(String accessToken) { return Observable.error(createNoDeviceException()); } - return sense.linkAccount(accessToken) - .map(Func.justValue(this)); + return serialize(sense.linkAccount(accessToken) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -358,8 +405,9 @@ public Observable linkPill(String accessToken) { return Observable.error(createNoDeviceException()); } - return sense.pairPill(accessToken) - .map(Func.justValue(this)); + return serialize(sense.pairPill(accessToken) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -368,8 +416,9 @@ public Observable pushData() { return Observable.error(createNoDeviceException()); } - return sense.pushData() - .map(Func.justValue(this)); + return serialize(sense.pushData() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -378,8 +427,9 @@ public Observable enablePairingMode() { return Observable.error(createNoDeviceException()); } - return sense.putIntoPairingMode() - .map(Func.justValue(this)); + return serialize(sense.putIntoPairingMode() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -388,8 +438,9 @@ public Observable disablePairingMode() { return Observable.error(createNoDeviceException()); } - return sense.putIntoNormalMode() - .map(Func.justValue(this)); + return serialize(sense.putIntoNormalMode() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } @CheckResult @@ -398,8 +449,9 @@ public Observable factoryReset() { return Observable.error(createNoDeviceException()); } - return sense.factoryReset() - .map(Func.justValue(this)); + return serialize(sense.factoryReset() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); } //endregion