aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtActionExecutor.java366
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtActivitySupport.java57
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtCallback.java53
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtDevice.java68
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtDeviceListener.java4
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtScanResult.java9
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtService.java55
-rw-r--r--app/src/main/java/io/trygvis/android/bt/BtUtils.java15
-rw-r--r--app/src/main/java/io/trygvis/android/bt/DefaultBtService.java192
-rw-r--r--app/src/main/java/io/trygvis/android/bt/NotOverriddenException.java4
-rw-r--r--app/src/main/java/io/trygvis/bluetooth/TrygvisIoUuids.java22
-rw-r--r--app/src/main/java/io/trygvis/soilmoisture/Constants.java13
-rw-r--r--app/src/main/java/io/trygvis/soilmoisture/ExceptionHandler.java22
-rw-r--r--app/src/main/java/io/trygvis/soilmoisture/MainActivity.java307
-rw-r--r--app/src/main/java/io/trygvis/soilmoisture/SmDevice.java26
-rw-r--r--app/src/main/java/io/trygvis/soilmoisture/SoilActivity.java38
16 files changed, 1251 insertions, 0 deletions
diff --git a/app/src/main/java/io/trygvis/android/bt/BtActionExecutor.java b/app/src/main/java/io/trygvis/android/bt/BtActionExecutor.java
new file mode 100644
index 0000000..8cbf87c
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtActionExecutor.java
@@ -0,0 +1,366 @@
+package io.trygvis.android.bt;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.util.Log;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Queue;
+
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onCharacteristicChanged;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onCharacteristicRead;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onCharacteristicWrite;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onConnectionStateChange;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onDescriptorRead;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onDescriptorWrite;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onFailure;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onReliableWriteCompleted;
+import static io.trygvis.android.bt.BtActionExecutor.EventType.onServicesDiscovered;
+
+public class BtActionExecutor {
+ private final static String TAG = BtActionExecutor.class.getSimpleName();
+ private final Queue<BtCallback> actionQ = new ArrayDeque<>();
+ private final Queue<BtCallback> finallyQ = new ArrayDeque<>();
+ private final List<BluetoothGattCallback> remoteRssi = new ArrayList<>();
+
+ public BtActionExecutor() {
+ }
+
+ public BtActionExecutor(BtCallback first) {
+ actionQ.add(first);
+ }
+
+// public static final BtCallback waitForIt = new BtCallback("Wait for it") {
+// @Override
+// public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+// Log.w(TAG, "wait for it...!");
+// return true;
+// }
+// };
+
+// public synchronized BtActionExecutor onSuccess(BtCallback btCallback) {
+// actionQ.add(btCallback);
+// return this;
+// }
+
+ public synchronized BtActionExecutor onConnectionStateChange(OnConnectionStateChange callback) {
+ actionQ.add(new BtCallback("onConnectionStateChange") {
+ @Override
+ public boolean onConnectionStateChange(BluetoothGatt gatt, int newState) {
+ return callback.onConnectionStateChange(gatt, newState);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onServicesDiscovered(OnServicesDiscovered callback) {
+ actionQ.add(new BtCallback("onServicesDiscovered") {
+ @Override
+ public boolean onServicesDiscovered(BluetoothGatt gatt) {
+ return callback.onServicesDiscovered(gatt);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onCharacteristicRead(OnCharacteristicRead callback) {
+ actionQ.add(new BtCallback("onCharacteristicRead") {
+ @Override
+ public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ return callback.onCharacteristicRead(gatt, characteristic);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onCharacteristicWrite(OnCharacteristicWrite callback) {
+ actionQ.add(new BtCallback("onCharacteristicWrite") {
+ @Override
+ public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ return callback.onCharacteristicWrite(gatt, characteristic);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onCharacteristicChanged(OnCharacteristicChanged callback) {
+ actionQ.add(new BtCallback("onCharacteristicChanged") {
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ return callback.onCharacteristicChanged(gatt, characteristic);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onDescriptorRead(OnDescriptorRead callback) {
+ actionQ.add(new BtCallback("onDescriptorRead") {
+ @Override
+ public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) {
+ return callback.onDescriptorRead(gatt, descriptor);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onDescriptorWrite(OnDescriptorWrite callback) {
+ actionQ.add(new BtCallback("onDescriptorWrite") {
+ @Override
+ public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) {
+ return callback.onDescriptorWrite(gatt, descriptor);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onReliableWriteCompleted(OnReliableWriteCompleted callback) {
+ actionQ.add(new BtCallback("onReliableWriteCompleted") {
+ @Override
+ public boolean onReliableWriteCompleted(BluetoothGatt gatt) {
+ return callback.onReliableWriteCompleted(gatt);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onReadRemoteRssi(OnReadRemoteRssi callback) {
+ actionQ.add(new BtCallback("onReadRemoteRssi") {
+ @Override
+ public boolean onReadRemoteRssi(BluetoothGatt gatt, int rssi) {
+ return callback.onReadRemoteRssi(gatt, rssi);
+ }
+ });
+ return this;
+ }
+
+ public synchronized BtActionExecutor onFailure(OnFailure callback) {
+ actionQ.add(new BtCallback("onFailure") {
+ @Override
+ public void onFailure() {
+ callback.onFailure();
+ }
+ });
+ return this;
+ }
+
+ public static interface OnConnectionStateChange {
+ boolean onConnectionStateChange(BluetoothGatt gatt, int newState);
+ }
+
+ public static interface OnServicesDiscovered {
+ boolean onServicesDiscovered(BluetoothGatt gatt);
+ }
+
+ public static interface OnCharacteristicRead {
+ boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);
+ }
+
+ public static interface OnCharacteristicWrite {
+ boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);
+ }
+
+ public static interface OnCharacteristicChanged {
+ boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);
+ }
+
+ public static interface OnDescriptorRead {
+ boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor);
+ }
+
+ public static interface OnDescriptorWrite {
+ boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor);
+ }
+
+ public static interface OnReliableWriteCompleted {
+ boolean onReliableWriteCompleted(BluetoothGatt gatt);
+ }
+
+ public static interface OnReadRemoteRssi {
+ boolean onReadRemoteRssi(BluetoothGatt gatt, int rssi);
+ }
+
+ public static interface OnFailure {
+ void onFailure();
+ }
+
+ public synchronized BtActionExecutor addFinally(BtCallback btCallback) {
+ finallyQ.add(btCallback);
+ return this;
+ }
+
+// public synchronized BtActionExecutor onRemoteRssi(BluetoothGattCallback callback) {
+// remoteRssi.add(callback);
+// return this;
+// }
+
+ void onEvent(EventType key, String values, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, int status, int newState) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, "Operation failed: " + key + ", " + values);
+ doFinally();
+ return;
+ }
+
+ Log.i(TAG, "Bt action completed successfully: callback=" + key);
+
+ BtCallback btCallback;
+ synchronized (this) {
+ if (actionQ.isEmpty()) {
+ Log.d(TAG, "All Bluetooth actions are done");
+
+ doFinally();
+ return;
+ }
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ btCallback = actionQ.remove();
+ Log.i(TAG, "Executing bt action: " + btCallback.name);
+ }
+
+ try {
+ boolean ok = callCallback(key, gatt, characteristic, descriptor, newState, btCallback);
+
+ if (!ok) {
+ Log.w(TAG, "The callback don't want to continue.");
+ doFinally();
+ }
+
+ if (actionQ.isEmpty()) {
+ Log.i(TAG, "The queue is empty");
+ }
+ } catch (NotOverriddenException e) {
+ Log.w(TAG, "Unexpected callback by listener: " + key);
+ doFinally();
+ }
+ }
+
+ private void doFinally() {
+ actionQ.clear();
+
+ for (BtCallback callback = finallyQ.poll(); callback != null; callback = finallyQ.poll()) {
+ try {
+ callCallback(onFailure, null, null, null, 0, callback);
+ } catch (NotOverriddenException e) {
+ return;
+ }
+ }
+ }
+
+ enum EventType {
+ onConnectionStateChange,
+ onServicesDiscovered,
+ onCharacteristicRead,
+ onCharacteristicWrite,
+ onCharacteristicChanged,
+ onDescriptorRead,
+ onDescriptorWrite,
+ onReliableWriteCompleted,
+
+ onFailure,
+ }
+
+ private Boolean callCallback(EventType key, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, int newState, BtCallback btCallback) {
+ switch (key) {
+ case onConnectionStateChange:
+ return btCallback.onConnectionStateChange(gatt, newState);
+ case onServicesDiscovered:
+ return btCallback.onServicesDiscovered(gatt);
+ case onCharacteristicRead:
+ return btCallback.onCharacteristicRead(gatt, characteristic);
+ case onCharacteristicWrite:
+ return btCallback.onCharacteristicWrite(gatt, characteristic);
+ case onCharacteristicChanged:
+ return btCallback.onCharacteristicChanged(gatt, characteristic);
+ case onDescriptorRead:
+ return btCallback.onDescriptorRead(gatt, descriptor);
+ case onDescriptorWrite:
+ return btCallback.onDescriptorWrite(gatt, descriptor);
+ case onReliableWriteCompleted:
+ return btCallback.onReliableWriteCompleted(gatt);
+
+ case onFailure:
+ btCallback.onFailure();
+ return null;
+ default:
+ Log.w(TAG, "Unknown callback: " + key);
+ return null;
+ }
+ }
+
+ public String toString() {
+ StringBuilder s = new StringBuilder("Queue: ");
+
+ Iterator<BtCallback> it = actionQ.iterator();
+
+ int i = 0;
+ while (it.hasNext()) {
+ BtCallback c = it.next();
+ if (i > 0) {
+ s.append(", ");
+ }
+ s.append(c.name);
+ i++;
+ }
+
+ return s.toString();
+ }
+
+ public BluetoothGattCallback asCallback() {
+ return new BluetoothGattCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ onEvent(onConnectionStateChange, "status=" + status + ", newState=" + newState, gatt, null, null, status, newState);
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ onEvent(onServicesDiscovered, "status=" + status, gatt, null, null, status, 9);
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ onEvent(onCharacteristicRead, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0);
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ onEvent(onCharacteristicWrite, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0);
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ onEvent(onCharacteristicChanged, "characteristic=" + characteristic.getUuid(), gatt, characteristic, null, BluetoothGatt.GATT_SUCCESS, 0);
+ }
+
+ @Override
+ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ onEvent(onDescriptorRead, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0);
+ }
+
+ @Override
+ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ onEvent(onDescriptorWrite, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0);
+ }
+
+ @Override
+ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+ onEvent(onReliableWriteCompleted, "status=" + status, gatt, null, null, status, 0);
+ }
+
+ @Override
+ public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+ for (BluetoothGattCallback callback : remoteRssi) {
+ callback.onReadRemoteRssi(gatt, rssi, status);
+ }
+ }
+ };
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/BtActivitySupport.java b/app/src/main/java/io/trygvis/android/bt/BtActivitySupport.java
new file mode 100644
index 0000000..03a36fb
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtActivitySupport.java
@@ -0,0 +1,57 @@
+package io.trygvis.android.bt;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.Toast;
+
+import io.trygvis.soilmoisture.R;
+
+public class BtActivitySupport {
+
+ private final Activity activity;
+ private final int requestCode;
+ private BluetoothAdapter mBluetoothAdapter;
+
+ public BtActivitySupport(Activity activity, int requestCode) {
+ this.activity = activity;
+ this.requestCode = requestCode;
+ }
+
+ public boolean onCreate() {
+ final BluetoothManager bluetoothManager = (BluetoothManager) activity.getSystemService(Context.BLUETOOTH_SERVICE);
+ mBluetoothAdapter = bluetoothManager.getAdapter();
+
+ // Checks if Bluetooth is supported on the device.
+ if (mBluetoothAdapter == null) {
+ Toast.makeText(activity, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).
+ show();
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean enableBt() {
+ // Ensures Bluetooth is enabled on the device. If Bluetooth is not currently enabled,
+ // fire an intent to display a dialog asking the user to grant permission to enable it.
+ if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ activity.startActivityForResult(enableBtIntent, requestCode);
+ }
+
+ return true;
+ }
+
+ @SuppressWarnings({"RedundantIfStatement", "UnusedParameters"})
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+ // User chose not to enable Bluetooth.
+ if (requestCode == this.requestCode && resultCode == Activity.RESULT_CANCELED) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/BtCallback.java b/app/src/main/java/io/trygvis/android/bt/BtCallback.java
new file mode 100644
index 0000000..7702962
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtCallback.java
@@ -0,0 +1,53 @@
+package io.trygvis.android.bt;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+
+public class BtCallback {
+ public final String name;
+
+ public BtCallback(String name) {
+ this.name = name;
+ }
+
+ public boolean onConnectionStateChange(BluetoothGatt gatt, int newState) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onServicesDiscovered(BluetoothGatt gatt) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onReliableWriteCompleted(BluetoothGatt gatt) {
+ throw new NotOverriddenException();
+ }
+
+ public boolean onReadRemoteRssi(BluetoothGatt gatt, int rssi) {
+ throw new NotOverriddenException();
+ }
+
+ public void onFailure() {
+ throw new NotOverriddenException();
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/BtDevice.java b/app/src/main/java/io/trygvis/android/bt/BtDevice.java
new file mode 100644
index 0000000..ba10d2d
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtDevice.java
@@ -0,0 +1,68 @@
+package io.trygvis.android.bt;
+
+import android.bluetooth.BluetoothDevice;
+import android.util.Log;
+
+public class BtDevice<A> {
+ private final static String TAG = BtDevice.class.getSimpleName();
+
+ private final DefaultBtService btService;
+ private final BluetoothDevice bluetoothDevice;
+ private Integer rssi;
+ private BtScanResult scanResult;
+ private A tag;
+
+ private boolean seenNow;
+
+ private BtDeviceListener l = null;
+
+ private BtDeviceListener listener = new BtDeviceListener() {
+ };
+
+ public BtDevice(DefaultBtService btService, BluetoothDevice bluetoothDevice, A tag, Integer rssi, BtScanResult scanResult) {
+ this.btService = btService;
+ this.bluetoothDevice = bluetoothDevice;
+ this.tag = tag;
+ this.rssi = rssi;
+ this.scanResult = scanResult;
+ }
+
+ public void addListener(BtDeviceListener listener) {
+ this.l = listener;
+ }
+
+ public A getTag() {
+ return tag;
+ }
+
+ public void setTag(A tag) {
+ this.tag = tag;
+ }
+
+ public String getAddress() {
+ return bluetoothDevice.getAddress();
+ }
+
+ public String getName() {
+ return bluetoothDevice.getName();
+ }
+
+ public int getRssi() {
+ return rssi;
+ }
+
+ public boolean connect(BtActionExecutor executor) {
+ Log.i(TAG, "connect(), address=" + bluetoothDevice.getAddress() + ", queue=" + executor);
+ bluetoothDevice.connectGatt(btService, false, executor.asCallback());
+ return true;
+ }
+
+ public BtScanResult getScanResult() {
+ return scanResult;
+ }
+
+ @Override
+ public String toString() {
+ return "BtDevice{address=" + bluetoothDevice.getAddress() + '}';
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/BtDeviceListener.java b/app/src/main/java/io/trygvis/android/bt/BtDeviceListener.java
new file mode 100644
index 0000000..57eabc6
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtDeviceListener.java
@@ -0,0 +1,4 @@
+package io.trygvis.android.bt;
+
+public interface BtDeviceListener {
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/BtScanResult.java b/app/src/main/java/io/trygvis/android/bt/BtScanResult.java
new file mode 100644
index 0000000..c443afa
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtScanResult.java
@@ -0,0 +1,9 @@
+package io.trygvis.android.bt;
+
+public class BtScanResult {
+ private final byte[] scanRecord;
+
+ public BtScanResult(byte[] scanRecord) {
+ this.scanRecord = scanRecord;
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/BtService.java b/app/src/main/java/io/trygvis/android/bt/BtService.java
new file mode 100644
index 0000000..123be3a
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtService.java
@@ -0,0 +1,55 @@
+package io.trygvis.android.bt;
+
+import android.os.Binder;
+
+import java.util.List;
+
+public interface BtService<A> {
+
+ boolean initialize(BtServiceListener<A> btServiceListener, Supplier<A> dataSupplier);
+
+ void clearCache();
+
+ boolean isScanning();
+
+ boolean startScanning(long timeoutMs);
+
+ void stopScanning();
+
+// BtDevice<A> getDevice(String macAddress);
+
+ List<BtDevice<A>> getDevices();
+
+ interface Supplier<A> {
+ A get();
+ }
+
+ interface BtServiceListener<A> {
+ void onScanStarted();
+
+ void onNewDevice(BtDevice<A> device);
+
+ void onScanStopped();
+ }
+
+ public abstract class AbstractBtServiceListener<A> implements BtServiceListener<A> {
+
+ public void onScanStarted() {
+ }
+
+ public void onScanStopped() {
+ }
+ }
+
+ public class LocalBinder<A> extends Binder {
+ private final BtService<A> service;
+
+ public LocalBinder(BtService<A> service) {
+ this.service = service;
+ }
+
+ public BtService<A> getService() {
+ return service;
+ }
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/BtUtils.java b/app/src/main/java/io/trygvis/android/bt/BtUtils.java
new file mode 100644
index 0000000..66e745f
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/BtUtils.java
@@ -0,0 +1,15 @@
+package io.trygvis.android.bt;
+
+public class BtUtils {
+ public static String toHexString(byte[] bytes) {
+ StringBuilder s = new StringBuilder();
+ for (byte b : bytes) {
+ if (b < 10) {
+ s.append('0');
+ }
+ s.append(Integer.toHexString(b));
+ }
+
+ return s.toString();
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/DefaultBtService.java b/app/src/main/java/io/trygvis/android/bt/DefaultBtService.java
new file mode 100644
index 0000000..8638544
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/DefaultBtService.java
@@ -0,0 +1,192 @@
+package io.trygvis.android.bt;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import io.trygvis.soilmoisture.R;
+
+public class DefaultBtService<A> extends Service implements BtService<A> {
+ private final static String TAG = DefaultBtService.class.getSimpleName();
+
+ private final IBinder binder = new LocalBinder<>(this);
+
+ private Handler handler = new Handler();
+
+ // -----------------------------------------------------------------------
+ // State
+ // -----------------------------------------------------------------------
+
+ private BtServiceListener<A> serviceListener = new AbstractBtServiceListener<A>() {
+ @Override
+ public void onNewDevice(BtDevice<A> device) {
+ }
+ };
+
+ private Supplier<A> tagConstructor;
+
+ private BluetoothManager bluetoothManager;
+
+ private BluetoothAdapter bluetoothAdapter;
+
+ private final List<BtDevice<A>> devices = new ArrayList<>();
+
+ private boolean scanning = false;
+
+ // -----------------------------------------------------------------------
+ // BtService Implementation
+ // -----------------------------------------------------------------------
+
+ @Override
+ public boolean initialize(BtServiceListener<A> serviceListener, Supplier<A> dataSupplier) {
+ if (bluetoothManager != null) {
+ Log.e(TAG, "Already initialized");
+ return false;
+ }
+
+ this.tagConstructor = dataSupplier;
+
+ if (serviceListener != null) {
+ this.serviceListener = serviceListener;
+ }
+
+ // Use this check to determine whether BLE is supported on the device. Then you can
+ // selectively disable BLE-related features.
+ if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
+ Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ // Initializes a Bluetooth adapter. For API level 18 and above, get a reference to
+ // BluetoothAdapter through BluetoothManager.
+ bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+
+ if (bluetoothManager == null) {
+ Log.e(TAG, "Unable to initialize BluetoothManager.");
+ return false;
+ }
+
+ bluetoothAdapter = bluetoothManager.getAdapter();
+ if (bluetoothAdapter == null) {
+ Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show();
+ bluetoothManager = null;
+ return false;
+ }
+
+ Log.e(TAG, "Bluetooth initialized");
+ return true;
+ }
+
+ @Override
+ public void clearCache() {
+ }
+
+ @Override
+ public boolean isScanning() {
+ return scanning;
+ }
+
+ @Override
+ public boolean startScanning(long timeoutMs) {
+ if (timeoutMs > 0) {
+ handler.postDelayed(this::stopScanning, timeoutMs);
+ }
+
+ if (bluetoothAdapter.startLeScan(leScanCallback)) {
+ scanning = true;
+ serviceListener.onScanStarted();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void stopScanning() {
+ // This doesn't mind being called twice.
+ bluetoothAdapter.stopLeScan(leScanCallback);
+
+ scanning = false;
+
+ serviceListener.onScanStopped();
+ }
+
+// @Override
+ public BtDevice<A> getDevice(String mac) {
+ BtDevice<A> device = findDevice(mac);
+
+ if (device != null) {
+ return device;
+ }
+
+ BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(mac);
+ return register(bluetoothDevice, null, null);
+ }
+
+ @Override
+ public List<BtDevice<A>> getDevices() {
+ return Collections.unmodifiableList(devices);
+ }
+
+ // -----------------------------------------------------------------------
+ // Scanning
+ // -----------------------------------------------------------------------
+
+ private BluetoothAdapter.LeScanCallback leScanCallback = (device, rssi, scanRecord) -> {
+// Log.i(TAG, "onLeScan()");
+
+ BtScanResult scanResult = new BtScanResult(scanRecord);
+
+ register(device, rssi, scanResult);
+ };
+
+ // -----------------------------------------------------------------------
+ // Service Implementation
+ // -----------------------------------------------------------------------
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ // -----------------------------------------------------------------------
+ // Stuff
+ // -----------------------------------------------------------------------
+
+ private BtDevice<A> register(BluetoothDevice bluetoothDevice, Integer rssi, BtScanResult scanResult) {
+ BtDevice<A> btDevice = findDevice(bluetoothDevice.getAddress());
+
+ if (btDevice != null) {
+ return btDevice;
+ }
+
+ Log.i(TAG, "New device: " + bluetoothDevice.getAddress());
+ btDevice = new BtDevice<>(this, bluetoothDevice, tagConstructor.get(), rssi, scanResult);
+ devices.add(btDevice);
+
+ serviceListener.onNewDevice(btDevice);
+
+ return btDevice;
+ }
+
+ private BtDevice<A> findDevice(String mac) {
+ for (BtDevice<A> d : devices) {
+ if (d.getAddress().equals(mac)) {
+ return d;
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/io/trygvis/android/bt/NotOverriddenException.java b/app/src/main/java/io/trygvis/android/bt/NotOverriddenException.java
new file mode 100644
index 0000000..0f2bb6a
--- /dev/null
+++ b/app/src/main/java/io/trygvis/android/bt/NotOverriddenException.java
@@ -0,0 +1,4 @@
+package io.trygvis.android.bt;
+
+class NotOverriddenException extends RuntimeException {
+}
diff --git a/app/src/main/java/io/trygvis/bluetooth/TrygvisIoUuids.java b/app/src/main/java/io/trygvis/bluetooth/TrygvisIoUuids.java
new file mode 100644
index 0000000..ec19b2a
--- /dev/null
+++ b/app/src/main/java/io/trygvis/bluetooth/TrygvisIoUuids.java
@@ -0,0 +1,22 @@
+package io.trygvis.bluetooth;
+
+import java.util.UUID;
+
+public interface TrygvisIoUuids {
+ String TRYGVIS_IO_BASE_UUID = "32D0xxxx-035D-59C5-70D3-BC8E4A1FD83F";
+
+ interface Services {
+ UUID STATUS_PANEL = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0001"));
+ UUID SOIL_MOISTURE_SERVICE = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0010"));
+ }
+
+ interface Characteristics {
+ UUID GAUGE_DATA = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0002"));
+ UUID GAUGE_CTRL = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0004"));
+ UUID LED = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0003"));
+
+ UUID SOIL_MOISTURE = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0011"));
+ }
+
+ UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+}
diff --git a/app/src/main/java/io/trygvis/soilmoisture/Constants.java b/app/src/main/java/io/trygvis/soilmoisture/Constants.java
new file mode 100644
index 0000000..0b891b7
--- /dev/null
+++ b/app/src/main/java/io/trygvis/soilmoisture/Constants.java
@@ -0,0 +1,13 @@
+package io.trygvis.soilmoisture;
+
+import java.util.UUID;
+
+public interface Constants {
+ String TRYGVIS_IO_BASE_UUID = "32D0xxxx-035D-59C5-70D3-BC8E4A1FD83F";
+ UUID TRYGVIS_IO_FIKEN_STATUS_PANEL_UUID = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0001"));
+ UUID TRYGVIS_IO_GAUGE_DATA_UUID = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0002"));
+ UUID TRYGVIS_IO_GAUGE_CTRL_UUID = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0004"));
+ UUID TRYGVIS_IO_LED_UUID = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0003"));
+
+ UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+}
diff --git a/app/src/main/java/io/trygvis/soilmoisture/ExceptionHandler.java b/app/src/main/java/io/trygvis/soilmoisture/ExceptionHandler.java
new file mode 100644
index 0000000..faa95e1
--- /dev/null
+++ b/app/src/main/java/io/trygvis/soilmoisture/ExceptionHandler.java
@@ -0,0 +1,22 @@
+package io.trygvis.soilmoisture;
+
+import android.util.Log;
+
+public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
+ private final static String TAG = ExceptionHandler.class.getSimpleName();
+
+ public static final ExceptionHandler EXCEPTION_HANDLER = new ExceptionHandler();
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ Log.e(TAG, "Uncaught", ex);
+
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ if (ex instanceof Error) {
+ throw (Error) ex;
+ }
+ throw new RuntimeException(ex);
+ }
+}
diff --git a/app/src/main/java/io/trygvis/soilmoisture/MainActivity.java b/app/src/main/java/io/trygvis/soilmoisture/MainActivity.java
new file mode 100644
index 0000000..18c96b8
--- /dev/null
+++ b/app/src/main/java/io/trygvis/soilmoisture/MainActivity.java
@@ -0,0 +1,307 @@
+package io.trygvis.soilmoisture;
+
+import android.app.ActionBar;
+import android.app.ListActivity;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattService;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.crashlytics.android.Crashlytics;
+
+import io.fabric.sdk.android.Fabric;
+import io.trygvis.android.bt.BtActionExecutor;
+import io.trygvis.android.bt.BtActivitySupport;
+import io.trygvis.android.bt.BtDevice;
+import io.trygvis.android.bt.BtDeviceListener;
+import io.trygvis.android.bt.BtService;
+import io.trygvis.android.bt.DefaultBtService;
+import io.trygvis.bluetooth.TrygvisIoUuids;
+
+import static io.trygvis.android.bt.BtService.BtServiceListener;
+import static io.trygvis.soilmoisture.ExceptionHandler.EXCEPTION_HANDLER;
+import static java.lang.String.valueOf;
+
+public class MainActivity extends ListActivity {
+ private final static String TAG = MainActivity.class.getSimpleName();
+
+ private static final long SCAN_PERIOD = 3 * 1000;
+
+ private static final int REQUEST_ENABLE_BT = 1;
+
+ private final BtActivitySupport btActivitySupport = new BtActivitySupport(this, REQUEST_ENABLE_BT);
+
+ private DeviceListAdapter deviceList;
+ private BtService<SmDevice> btService;
+
+ private ServiceConnection serviceConnection;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ Thread.setDefaultUncaughtExceptionHandler(EXCEPTION_HANDLER);
+ super.onCreate(savedInstanceState);
+ Fabric.with(this, new Crashlytics());
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.title_devices);
+ }
+
+ if (!btActivitySupport.onCreate()) {
+ finish();
+ return;
+ }
+
+ serviceConnection = new ServiceConnection() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ btService = ((BtService.LocalBinder<SmDevice>) service).getService();
+ if (!btService.initialize(serviceListener, SmDevice::new)) {
+ finish();
+ }
+
+ deviceList = new DeviceListAdapter();
+ deviceList.notifyDataSetChanged();
+ setListAdapter(deviceList);
+
+ startScan();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ btService = null;
+ stopScan();
+ }
+ };
+
+ bindService(new Intent(this, DefaultBtService.class), serviceConnection, BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(TAG, "onDestroy");
+ super.onDestroy();
+
+ if (serviceConnection != null) {
+ unbindService(serviceConnection);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Log.i(TAG, "onResume");
+
+ super.onResume();
+
+ if (!btActivitySupport.enableBt()) {
+ finish();
+ return;
+ }
+
+ // registerReceiver(btServiceBroadcastReceiver, IntentAction.ALL_FILTER);
+ }
+
+ @Override
+ protected void onPause() {
+ Log.i(TAG, "onPause");
+
+ super.onPause();
+ stopScan();
+ // unregisterReceiver(ntServiceBroadcastReceiver);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.i(TAG, "onActivityResult");
+
+ if (!btActivitySupport.onActivityResult(requestCode, resultCode, data)) {
+ finish();
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ Log.i(TAG, "onCreateOptionsMenu");
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.main, menu);
+
+ if (!btService.isScanning()) {
+ menu.findItem(R.id.menu_stop).setVisible(false);
+ menu.findItem(R.id.menu_scan).setVisible(true);
+ menu.findItem(R.id.menu_refresh).setActionView(null);
+ } else {
+ menu.findItem(R.id.menu_stop).setVisible(true);
+ menu.findItem(R.id.menu_scan).setVisible(false);
+ menu.findItem(R.id.menu_refresh).setActionView(R.layout.actionbar_indeterminate_progress);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ Log.i(TAG, "onOptionsItemSelected");
+
+ switch (item.getItemId()) {
+ case R.id.menu_scan:
+ startScan();
+ break;
+ case R.id.menu_stop:
+ stopScan();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void startScan() {
+ btService.startScanning(SCAN_PERIOD);
+ }
+
+ private void stopScan() {
+ if (btService != null) {
+ btService.stopScanning();
+ }
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ stopScan();
+
+ BtDevice<SmDevice> state = btService.getDevices().get(position);
+
+ BtActionExecutor executor = new BtActionExecutor().
+ onConnectionStateChange((gatt, newState) -> {
+ if (newState == BluetoothGatt.STATE_CONNECTED) {
+ Intent intent = new Intent(this, SoilActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ return false;
+ }).
+ onServicesDiscovered(gatt -> false);
+
+ state.connect(executor);
+ }
+
+ BtServiceListener<SmDevice> serviceListener = new BtServiceListener<SmDevice>() {
+ @Override
+ public void onScanStarted() {
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onNewDevice(BtDevice<SmDevice> device) {
+ device.addListener(deviceListener);
+
+ BtActionExecutor executor = new BtActionExecutor().
+ onConnectionStateChange((gatt, newState) -> gatt.discoverServices()).
+ onServicesDiscovered(gatt -> {
+ BluetoothGattService service = gatt.getService(TrygvisIoUuids.Services.SOIL_MOISTURE_SERVICE);
+
+ boolean useful = service != null;
+ device.getTag().setIsUseful(useful);
+ runOnUiThread(deviceList::notifyDataSetChanged);
+ return useful;
+ });
+ device.connect(executor);
+ }
+
+ @Override
+ public void onScanStopped() {
+ invalidateOptionsMenu();
+ }
+ };
+
+ BtDeviceListener deviceListener = new BtDeviceListener() {
+ };
+
+ // -----------------------------------------------------------------------
+ //
+ // -----------------------------------------------------------------------
+
+ static class DeviceListItem {
+ final TextView deviceName;
+ final TextView deviceAddress;
+ final TextView rssi;
+ final ProgressBar spinner;
+ final Button connect;
+
+ DeviceListItem(View view) {
+ this.deviceName = (TextView) view.findViewById(R.id.device_name);
+ this.deviceAddress = (TextView) view.findViewById(R.id.device_address);
+ this.rssi = (TextView) view.findViewById(R.id.device_rssi);
+ this.spinner = (ProgressBar) view.findViewById(R.id.device_spinner);
+ this.connect = (Button) view.findViewById(R.id.button_connect);
+ }
+ }
+
+ private class DeviceListAdapter extends BaseAdapter {
+ private LayoutInflater inflater = MainActivity.this.getLayoutInflater();
+
+ @Override
+ public int getCount() {
+ return btService.getDevices().size();
+ }
+
+ @Override
+ public Object getItem(int i) {
+ return btService.getDevices().get(i);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public View getView(int i, View view, ViewGroup viewGroup) {
+ DeviceListItem item;
+
+ if (view == null) {
+ view = inflater.inflate(R.layout.listitem_device, null);
+ item = new DeviceListItem(view);
+ view.setTag(item);
+ view.setClickable(false);
+ } else {
+ item = (DeviceListItem) view.getTag();
+ }
+
+ BtDevice<SmDevice> btDevice = btService.getDevices().get(i);
+ if (btDevice.getName() != null && btDevice.getName().length() > 0) {
+ item.deviceName.setText(btDevice.getName());
+ } else {
+ item.deviceName.setText(R.string.unknown_device);
+ }
+ item.deviceAddress.setText(btDevice.getAddress());
+
+ item.rssi.setText(getText(R.string.rssi) + ": " +
+ (btDevice.getRssi() != 0 ? valueOf(btDevice.getRssi()) : getText(R.string.unknown)));
+
+ SmDevice smDevice = btDevice.getTag();
+
+ boolean useful = smDevice.isUseful();
+ item.spinner.setVisibility(useful ? View.GONE : View.VISIBLE);
+ item.connect.setVisibility(useful ? View.VISIBLE : View.GONE);
+ view.setClickable(useful);
+
+ return view;
+ }
+ }
+}
diff --git a/app/src/main/java/io/trygvis/soilmoisture/SmDevice.java b/app/src/main/java/io/trygvis/soilmoisture/SmDevice.java
new file mode 100644
index 0000000..6bc522d
--- /dev/null
+++ b/app/src/main/java/io/trygvis/soilmoisture/SmDevice.java
@@ -0,0 +1,26 @@
+package io.trygvis.soilmoisture;
+
+import android.util.Log;
+
+class SmDevice {
+ private final static String TAG = SmDevice.class.getSimpleName();
+
+ public SmDevice() {
+ Log.i(TAG, "new device");
+ }
+
+ private Boolean isUseful;
+
+ public boolean isUseful() {
+ return isUseful != null && isUseful;
+ }
+
+ public Boolean getIsUseful() {
+ return isUseful;
+ }
+
+ public void setIsUseful(Boolean isUseful) {
+ Log.i(TAG, "useful=" + isUseful);
+ this.isUseful = isUseful;
+ }
+}
diff --git a/app/src/main/java/io/trygvis/soilmoisture/SoilActivity.java b/app/src/main/java/io/trygvis/soilmoisture/SoilActivity.java
new file mode 100644
index 0000000..4c66f7b
--- /dev/null
+++ b/app/src/main/java/io/trygvis/soilmoisture/SoilActivity.java
@@ -0,0 +1,38 @@
+package io.trygvis.soilmoisture;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+
+public class SoilActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_soil);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_soil, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == R.id.action_settings) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}