diff --git a/ble/build.gradle b/ble/build.gradle index 61f9b6c..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.3.1" // ... +def VERSION_NAME = "2016.3.7.1-SNAPSHOT" // ... def PACKAGE = 'is.hello.commonsense' apply plugin: 'com.android.library' @@ -12,13 +12,12 @@ def generateVersionCode() { def formatter = new SimpleDateFormat("yyMMddHH") Integer.parseInt(formatter.format(now)) } - android { compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig { - minSdkVersion 18 + minSdkVersion 19 targetSdkVersion 23 versionCode generateVersionCode() versionName VERSION_NAME @@ -44,7 +43,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 +89,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/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 @@ + + + + + + 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..00ac616 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; @@ -28,7 +29,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; @@ -41,8 +41,9 @@ 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.Functions; +import is.hello.commonsense.util.Func; import rx.Observable; import rx.Observer; import rx.Subscriber; @@ -54,13 +55,12 @@ 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(); - static { - BuruberiReportingProvider.register(); - } - //region Versions /** @@ -112,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, @@ -125,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) { @@ -183,16 +191,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()); + } } }; @@ -203,18 +217,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(Functions.createMapperToValue(ConnectProgress.CONNECTING)), - gattPeripheral.connect(connectFlags, timeout).map(Functions.createMapperToValue(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(Functions.createMapperToValue(ConnectProgress.BONDING)), - gattPeripheral.createBond().map(Functions.createMapperToValue(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()) @@ -252,7 +266,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 +283,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 +638,7 @@ public Observable putIntoNormalMode() { .setAppVersion(APP_VERSION) .build(); return performSimpleCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -642,7 +656,7 @@ public Observable putIntoPairingMode() { .setAppVersion(APP_VERSION) .build(); return performDisconnectingCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -870,7 +884,7 @@ public Observable linkAccount(final String accountToken) { .setAccountId(accountToken) .build(); return performSimpleCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -888,7 +902,7 @@ public Observable factoryReset() { .setAppVersion(APP_VERSION) .build(); return performDisconnectingCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -906,7 +920,7 @@ public Observable pushData() { .setAppVersion(APP_VERSION) .build(); return performSimpleCommand(morpheusCommand, createSimpleCommandTimeout()) - .map(Functions.createMapperToVoid()); + .map(Func.justVoid()); } @CheckResult @@ -924,7 +938,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 new file mode 100644 index 0000000..dfb469e --- /dev/null +++ b/ble/src/main/java/is/hello/commonsense/service/SenseService.java @@ -0,0 +1,476 @@ +package is.hello.commonsense.service; + +import android.app.Notification; +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.annotation.VisibleForTesting; +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.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; +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.SensePeripheral.CountryCode; +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.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 + * 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 ForegroundNotificationProvider notificationProvider; + @VisibleForTesting boolean foregroundEnabled = false; + + //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(); + + final IntentFilter connectionIntentFilter = new IntentFilter(); + connectionIntentFilter.addAction(GattPeripheral.ACTION_CONNECTED); + connectionIntentFilter.addAction(GattPeripheral.ACTION_DISCONNECTED); + LocalBroadcastManager.getInstance(this) + .registerReceiver(connectionBroadcastReceiver, connectionIntentFilter); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + LocalBroadcastManager.getInstance(this) + .unregisterReceiver(connectionBroadcastReceiver); + + if (sense != null && sense.isConnected()) { + Log.w(LOG_TAG, "Service being destroyed with active connection"); + } + } + + //endregion + + + //region Utilities + + 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 + * only run when all work submitted before it has completed. + */ + public Observable serialize(@NonNull Observable observable) { + return Rx.serialize(observable, queue); + } + + //endregion + + + //region Foregrounding + + /** + * 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; + + if (enabled) { + if (notificationProvider == null) { + throw new IllegalStateException("Cannot enable foregrounding without a notification provider"); + } + + Log.d(LOG_TAG, "Foregrounding enabled"); + startForeground(notificationProvider.getId(), + notificationProvider.getNotification()); + } else { + Log.d(LOG_TAG, "Foregrounding disabled"); + stopForeground(true); + } + } + } + + /** + * 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 provider The new provider. If {@code null}, foreground status will be discontinued. + */ + public void setForegroundNotificationProvider(@Nullable ForegroundNotificationProvider provider) { + this.notificationProvider = provider; + + if (provider == null && foregroundEnabled) { + setForegroundEnabled(false); + } + } + + //endregion + + + //region Managing Connectivity + + 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)) { + 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()); + 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, + SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT); + 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); + criteria.addStartsWithPredicate(AdvertisingData.TYPE_SERVICE_DATA, + SenseIdentifiers.ADVERTISEMENT_SERVICE_16_BIT + 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()) { + return Observable.error(new IllegalStateException("Cannot connect to multiple Senses at once.")); + } + + this.sense = new SensePeripheral(peripheral); + + return serialize(sense.connect()); + } + + /** + * 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(this); + } + + // 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)) + .doOnCompleted(new Action0() { + @Override + public void call() { + queue.cancelPending(); + } + }); + } + + /** + * 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; + } + + //endregion + + + //region Commands + + @CheckResult + public Observable trippyLEDs() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.runLedAnimation(SenseLedAnimation.TRIPPY) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable busyLEDs() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.runLedAnimation(SenseLedAnimation.BUSY) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable fadeOutLEDs() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.runLedAnimation(SenseLedAnimation.FADE_OUT) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable stopLEDs() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.runLedAnimation(SenseLedAnimation.STOP) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable> scanForWifiNetworks(@Nullable CountryCode countryCode) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.scanForWifiNetworks(countryCode) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable currentWifiNetwork() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.getWifiNetwork() + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable sendWifiCredentials(@NonNull String ssid, + @NonNull wifi_endpoint.sec_type securityType, + @Nullable String password) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.connectToWiFiNetwork(ssid, securityType, password) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable linkAccount(String accessToken) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.linkAccount(accessToken) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable linkPill(String accessToken) { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.pairPill(accessToken) + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable pushData() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.pushData() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable enablePairingMode() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.putIntoPairingMode() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable disablePairingMode() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.putIntoNormalMode() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + @CheckResult + public Observable factoryReset() { + if (sense == null) { + return Observable.error(createNoDeviceException()); + } + + return serialize(sense.factoryReset() + .map(Func.justValue(this)) + .doOnError(createCleanUpHandler())); + } + + //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/main/java/is/hello/commonsense/service/SenseServiceConnection.java b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java new file mode 100644 index 0000000..3505216 --- /dev/null +++ b/ble/src/main/java/is/hello/commonsense/service/SenseServiceConnection.java @@ -0,0 +1,170 @@ +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 android.support.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +import rx.Observable; +import rx.functions.Func1; +import rx.subjects.AsyncSubject; + +/** + * Helper class to facilitate communication between a component and the {@link SenseService}. + */ +public class SenseServiceConnection implements ServiceConnection { + private final Context context; + private final List listeners = new ArrayList<>(); + @VisibleForTesting @Nullable SenseService senseService; + + //region Lifecycle + + /** + * Construct a service helper. + * @param context The context whose lifecycle this helper will be bound to. + */ + public SenseServiceConnection(@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 = listeners.size() - 1; i >= 0; i--) { + listeners.get(i).onSenseServiceConnected(senseService); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + this.senseService = null; + + for (int i = listeners.size() - 1; i >= 0; i--) { + listeners.get(i).onSenseServiceDisconnected(); + } + } + + //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 listener The consumer. + */ + public void registerConsumer(@NonNull Listener listener) { + listeners.add(listener); + + if (senseService != null) { + listener.onSenseServiceConnected(senseService); + } + } + + /** + * Unregister a consumer from the helper. Safe to call within {@link Listener} callbacks. + * @param listener The consumer. + */ + public void unregisterConsumer(@NonNull Listener listener) { + listeners.remove(listener); + } + + /** + * 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; + } + } + + /** + * 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. + */ + public boolean isConnectedToSense() { + return (senseService != null && senseService.isConnected()); + } + + //endregion + + + /** + * Specifies a class that is interested in communicating with the {@link SenseService}. + */ + public interface Listener { + /** + * Called when the {@link SenseService} is available for use. + * @param service The service. + */ + void onSenseServiceConnected(@NonNull SenseService service); + + /** + * Called when the {@link SenseService} becomes unavailable. Any external + * references to it should be immediately cleared. + */ + void onSenseServiceDisconnected(); + } +} 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) { 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/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/CommonSensePeripheralTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/SensePeripheralTests.java similarity index 74% 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..6c5d12f 100644 --- a/ble/src/test/java/is/hello/commonsense/bluetooth/CommonSensePeripheralTests.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.CommonSenseTestCase; 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; @@ -45,74 +42,25 @@ import static org.mockito.Mockito.verify; @SuppressWarnings("ResourceType") -public class CommonSensePeripheralTests 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 - - +public class SensePeripheralTests extends CommonSenseTestCase { //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) @@ -263,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) @@ -283,8 +234,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 +258,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 +282,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 +307,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 +321,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 +344,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 +388,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 +416,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 +452,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 +477,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 +498,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 +530,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/bluetooth/model/CommonSensePacketHandlerTests.java b/ble/src/test/java/is/hello/commonsense/bluetooth/model/ProtobufPacketListenerTests.java similarity index 74% 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..88f3420 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 @@ -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; @@ -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); 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..6514cfb --- /dev/null +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceConnectionTests.java @@ -0,0 +1,192 @@ +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.Observable; +import rx.Observer; +import rx.functions.Func1; + +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.never; +import static org.mockito.Mockito.spy; +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.senseService, is(notNullValue())); + //noinspection ConstantConditions + verify(listener).onSenseServiceConnected(connection.senseService); + + connection.destroy(); + assertThat(connection.senseService, is(nullValue())); + verify(listener).onSenseServiceDisconnected(); + } + + @Test + public void senseServiceCold() { + final SenseServiceConnection connection = new SenseServiceConnection(getContext()); + assertThat(connection.senseService, 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.senseService, 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 performCold() { + final SenseServiceConnection connection = spy(new SenseServiceConnection(getContext())); + assertThat(connection.senseService, 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.senseService, 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()); + assertThat(connection.isConnectedToSense(), is(false)); + + connection.create(); + assertThat(connection.senseService, is(notNullValue())); + assertThat(connection.isConnectedToSense(), is(false)); + + final SensePeripheral fakePeripheral = mock(SensePeripheral.class); + doReturn(true).when(fakePeripheral).isConnected(); + //noinspection ConstantConditions + connection.senseService.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 new file mode 100644 index 0000000..19c7ab7 --- /dev/null +++ b/ble/src/test/java/is/hello/commonsense/service/SenseServiceTests.java @@ -0,0 +1,267 @@ +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; +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.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.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; + + //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 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() + .add(AdvertisingData.TYPE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + SenseIdentifiers.ADVERTISEMENT_SERVICE_128_BIT) + .build(); + + final PeripheralCriteria criteriaWithoutDeviceId = SenseService.createSenseCriteria(); + 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 = SenseService.createSenseCriteria(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 = spy(new SensePeripheral(device)); + doReturn(true).when(service.sense).isConnected(); + 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(); + } + }); + + service.onPeripheralConnected(); + + 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(); + 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(service))); + } + + @Test + public void setForegroundNotificationProviderDisablesForegroundOnNull() { + service.foregroundEnabled = true; + service.setForegroundNotificationProvider(null); + assertThat(service.foregroundEnabled, is(false)); + } + + @Test(expected = ConnectionStateException.class) + public void trippyLEDsRequiresDevice() { + Sync.last(service.trippyLEDs()); + } + + @Test(expected = ConnectionStateException.class) + public void busyLEDsRequiresDevice() { + Sync.last(service.busyLEDs()); + } + + @Test(expected = ConnectionStateException.class) + public void fadeOutLEDsRequiresDevice() { + Sync.last(service.fadeOutLEDs()); + } + + @Test(expected = ConnectionStateException.class) + public void stopLEDsRequiresDevice() { + Sync.last(service.stopLEDs()); + } + + + @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 enablePairingModeRequiresDevice() { + Sync.last(service.enablePairingMode()); + } + + @Test(expected = ConnectionStateException.class) + public void disablePairingModeRequiresDevice() { + Sync.last(service.disablePairingMode()); + } + + @Test(expected = ConnectionStateException.class) + public void factoryResetRequiresDevice() { + Sync.last(service.factoryReset()); + } +} 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..cb77aed 100644 --- a/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java +++ b/ble/src/test/java/is/hello/commonsense/util/ErrorsTests.java @@ -8,7 +8,9 @@ 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 is.hello.commonsense.bluetooth.errors.BuruberiReportingProvider; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; @@ -16,10 +18,13 @@ 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 { + public ErrorsTests() { + BuruberiReportingProvider.register(); + } + @Test public void getType() throws Exception { assertEquals("java.lang.Throwable", Errors.getType(new Throwable())); 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;