aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/.gitignore4
-rw-r--r--app/app.iml94
-rw-r--r--app/build.gradle50
-rw-r--r--app/proguard-rules.pro17
-rw-r--r--app/src/androidTest/java/io/trygvis/soilmoisture/ApplicationTest.java13
-rw-r--r--app/src/main/AndroidManifest.xml49
-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
-rw-r--r--app/src/main/main.iml17
-rw-r--r--app/src/main/res/demo/demo/activity_main2.xml36
-rw-r--r--app/src/main/res/demo/values/base-strings.xml31
-rw-r--r--app/src/main/res/demo/values/template-dimens.xml32
-rw-r--r--app/src/main/res/demo/values/template-styles.xml44
-rw-r--r--app/src/main/res/drawable-hdpi/ic_launcher.pngbin0 -> 9397 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_launcher.pngbin0 -> 5237 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_launcher.pngbin0 -> 14383 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 19388 bytes
-rw-r--r--app/src/main/res/layout/actionbar_indeterminate_progress.xml23
-rw-r--r--app/src/main/res/layout/activity_main.xml7
-rw-r--r--app/src/main/res/layout/activity_soil.xml60
-rw-r--r--app/src/main/res/layout/fragment_gauge.xml24
-rw-r--r--app/src/main/res/layout/listitem_device.xml110
-rw-r--r--app/src/main/res/menu/gatt_services.xml29
-rw-r--r--app/src/main/res/menu/main.xml29
-rw-r--r--app/src/main/res/menu/menu_main.xml5
-rw-r--r--app/src/main/res/menu/menu_soil.xml8
-rw-r--r--app/src/main/res/values-w820dp/dimens.xml6
-rw-r--r--app/src/main/res/values/dimens.xml5
-rw-r--r--app/src/main/res/values/strings.xml35
-rw-r--r--app/src/main/res/values/styles.xml8
44 files changed, 1987 insertions, 0 deletions
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..0e5d9b5
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,4 @@
+/build
+crashlytics-build.properties
+crashlytics.properties
+com_crashlytics_export_strings.xml
diff --git a/app/app.iml b/app/app.iml
new file mode 100644
index 0000000..eaea10c
--- /dev/null
+++ b/app/app.iml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android" name="Android">
+ <configuration>
+ <option name="SELECTED_BUILD_VARIANT" value="debug" />
+ <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
+ <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
+ <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" />
+ <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
+ <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugTestSources" />
+ <option name="ALLOW_USER_CONFIGURATION" value="false" />
+ <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
+ <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
+ <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
+ <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+ </configuration>
+ </facet>
+ <facet type="android-gradle" name="Android-Gradle">
+ <configuration>
+ <option name="GRADLE_PROJECT_PATH" value=":app" />
+ </configuration>
+ </facet>
+ </component>
+ <component name="NewModuleRootManager" inherit-compiler-output="false">
+ <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/debug" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/debug" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
+ <excludeFolder url="file://$MODULE_DIR$/build/dependency-cache" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
+ <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/retrolambda" />
+ <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
+ </content>
+ <orderEntry type="jdk" jdkName="Android API 18 Platform" jdkType="Android SDK" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" exported="" name="answers-1.0.2" level="project" />
+ <orderEntry type="library" exported="" name="fabric-1.0.2" level="project" />
+ <orderEntry type="library" exported="" name="crashlytics-2.1.0" level="project" />
+ <orderEntry type="library" exported="" name="beta-1.0.2" level="project" />
+ </component>
+</module>
+
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..320e9a4
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,50 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ maven { url 'https://maven.fabric.io/public' }
+ }
+
+ dependencies {
+ classpath 'io.fabric.tools:gradle:1.14.4'
+ classpath 'me.tatarka:gradle-retrolambda:2.5.0'
+ }
+}
+apply plugin: 'com.android.application'
+apply plugin: 'io.fabric'
+apply plugin: 'me.tatarka.retrolambda'
+
+repositories {
+ maven { url 'https://maven.fabric.io/public' }
+}
+
+android {
+ compileSdkVersion 18
+ buildToolsVersion "19.1.0"
+
+ defaultConfig {
+ applicationId "io.trygvis.soilmoisture"
+ minSdkVersion 18
+ targetSdkVersion 18
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+// runProguard false
+// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ compile('com.crashlytics.sdk.android:crashlytics:2.1.0@aar') {
+ transitive = true;
+ }
+// compile 'net.sourceforge.streamsupport:streamsupport:1.1.2'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..875c86a
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/trygvis/opt/android-sdk-linux/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/app/src/androidTest/java/io/trygvis/soilmoisture/ApplicationTest.java b/app/src/androidTest/java/io/trygvis/soilmoisture/ApplicationTest.java
new file mode 100644
index 0000000..e7864d4
--- /dev/null
+++ b/app/src/androidTest/java/io/trygvis/soilmoisture/ApplicationTest.java
@@ -0,0 +1,13 @@
+package io.trygvis.soilmoisture;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c04494d
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.trygvis.soilmoisture">
+
+ <!--
+ Declare this required feature if you want to make the app available to BLE-capable
+ devices only. If you want to make your app available to devices that don't support BLE,
+ you should omit this in the manifest. Instead, determine BLE capability by using
+ PackageManager.hasSystemFeature(FEATURE_BLUETOOTH_LE)
+ -->
+ <uses-feature
+ android:name="android.hardware.bluetooth_le"
+ android:required="true"/>
+
+ <uses-permission android:name="android.permission.BLUETOOTH"/>
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme">
+
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".SoilActivity"
+ android:label="@string/title_activity_soil">
+ </activity>
+
+ <service
+ android:name="io.trygvis.android.bt.DefaultBtService"
+ android:enabled="true"/>
+
+ <meta-data
+ android:name="com.crashlytics.ApiKey"
+ android:value="cf760ececcb6d74c66781b3e21ae115aaae3ffd3"/>
+ </application>
+
+</manifest>
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);
+ }
+}
diff --git a/app/src/main/main.iml b/app/src/main/main.iml
new file mode 100644
index 0000000..81597c9
--- /dev/null
+++ b/app/src/main/main.iml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android" name="Android">
+ <configuration />
+ </facet>
+ </component>
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
+ </content>
+ <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
+
diff --git a/app/src/main/res/demo/demo/activity_main2.xml b/app/src/main/res/demo/demo/activity_main2.xml
new file mode 100644
index 0000000..b48f349
--- /dev/null
+++ b/app/src/main/res/demo/demo/activity_main2.xml
@@ -0,0 +1,36 @@
+<!--
+ Copyright 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout style="@style/Widget.SampleMessageTile"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView style="@style/Widget.SampleMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/horizontal_page_margin"
+ android:layout_marginRight="@dimen/horizontal_page_margin"
+ android:layout_marginTop="@dimen/vertical_page_margin"
+ android:layout_marginBottom="@dimen/vertical_page_margin"
+ android:text="@string/intro_message" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/app/src/main/res/demo/values/base-strings.xml b/app/src/main/res/demo/values/base-strings.xml
new file mode 100644
index 0000000..6071269
--- /dev/null
+++ b/app/src/main/res/demo/values/base-strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+<!--
+ <string name="app_name">BluetoothLeGatt</string>
+-->
+ <string name="intro_message">
+ <![CDATA[
+
+
+ This sample demonstrates how to use the Bluetooth LE Generic Attribute Profile (GATT)
+ to transmit arbitrary data between devices.
+
+
+ ]]>
+ </string>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/demo/values/template-dimens.xml b/app/src/main/res/demo/values/template-dimens.xml
new file mode 100644
index 0000000..afacaf5
--- /dev/null
+++ b/app/src/main/res/demo/values/template-dimens.xml
@@ -0,0 +1,32 @@
+<!--
+ Copyright 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+
+ <!-- Define standard dimensions to comply with Holo-style grids and rhythm. -->
+
+ <dimen name="margin_tiny">4dp</dimen>
+ <dimen name="margin_small">8dp</dimen>
+ <dimen name="margin_medium">16dp</dimen>
+ <dimen name="margin_large">32dp</dimen>
+ <dimen name="margin_huge">64dp</dimen>
+
+ <!-- Semantic definitions -->
+
+ <dimen name="horizontal_page_margin">@dimen/margin_medium</dimen>
+ <dimen name="vertical_page_margin">@dimen/margin_medium</dimen>
+
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/demo/values/template-styles.xml b/app/src/main/res/demo/values/template-styles.xml
new file mode 100644
index 0000000..f23e542
--- /dev/null
+++ b/app/src/main/res/demo/values/template-styles.xml
@@ -0,0 +1,44 @@
+<!--
+ Copyright 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+
+ <!-- Activity themes -->
+
+ <style name="Theme.Base" parent="android:Theme.Light"/>
+
+ <style name="Theme.Sample" parent="Theme.Base"/>
+
+ <!--
+ <style name="AppTheme" parent="Theme.Sample" />
+ -->
+ <!-- Widget styling -->
+
+ <style name="Widget"/>
+
+ <style name="Widget.SampleMessage">
+ <item name="android:textAppearance">?android:textAppearanceMedium</item>
+ <item name="android:lineSpacingMultiplier">1.1</item>
+ </style>
+
+ <style name="Widget.SampleMessageTile">
+ <item name="android:background">@drawable/tile</item>
+ <item name="android:shadowColor">#7F000000</item>
+ <item name="android:shadowDy">-3.5</item>
+ <item name="android:shadowRadius">2</item>
+ </style>
+
+</resources>
diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4df1894
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/layout/actionbar_indeterminate_progress.xml b/app/src/main/res/layout/actionbar_indeterminate_progress.xml
new file mode 100644
index 0000000..c68bc3c
--- /dev/null
+++ b/app/src/main/res/layout/actionbar_indeterminate_progress.xml
@@ -0,0 +1,23 @@
+<!--
+ Copyright 2013 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="56dp"
+ android:minWidth="56dp">
+ <ProgressBar android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_gravity="center"/>
+</FrameLayout>
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..ead92a8
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,7 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity"
+ tools:ignore="MergeRootFrame"/>
diff --git a/app/src/main/res/layout/activity_soil.xml b/app/src/main/res/layout/activity_soil.xml
new file mode 100644
index 0000000..c2784b9
--- /dev/null
+++ b/app/src/main/res/layout/activity_soil.xml
@@ -0,0 +1,60 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ tools:context="io.trygvis.soilmoisture.SoilActivity">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:text="Large Text"
+ android:id="@+id/name"
+ android:layout_alignParentStart="true"
+ />
+
+ <ProgressBar
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/currentValueHorizontal"
+ android:layout_alignBottom="@+id/textView"
+ android:layout_toEndOf="@+id/space"/>
+
+ <ProgressBar
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/currentValueSpinner"
+ android:layout_above="@+id/button_refresh"
+ android:layout_centerHorizontal="true"/>
+
+ <Button
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Refresh"
+ android:id="@+id/button_refresh"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/current_value"
+ android:id="@+id/textView"
+ android:layout_below="@+id/name"
+ android:layout_alignParentStart="true"/>
+
+ <Space
+ android:layout_width="20px"
+ android:layout_height="20px"
+ android:layout_alignTop="@+id/currentValueHorizontal"
+ android:layout_toEndOf="@+id/textView"
+ android:id="@+id/space"
+ android:layout_alignBottom="@+id/textView"/>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/fragment_gauge.xml b/app/src/main/res/layout/fragment_gauge.xml
new file mode 100644
index 0000000..ff1df86
--- /dev/null
+++ b/app/src/main/res/layout/fragment_gauge.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ >
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Gauge: XX"
+ android:id="@+id/gauge_title"
+ android:layout_gravity="center_horizontal"
+ android:gravity="start"/>
+
+ <SeekBar
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/gauge_seek_bar"
+ android:layout_gravity="center_horizontal"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/listitem_device.xml b/app/src/main/res/layout/listitem_device.xml
new file mode 100644
index 0000000..2d6ef91
--- /dev/null
+++ b/app/src/main/res/layout/listitem_device.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content">
+
+ <TextView
+ android:id="@+id/device_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="24sp"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="0dp"
+ android:layout_toStartOf="@+id/spacer"/>
+
+ <TextView
+ android:id="@+id/device_address"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:layout_below="@+id/device_name"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@+id/spacer"/>
+
+ <TextView
+ android:id="@+id/device_rssi"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:layout_below="@+id/device_address"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@+id/spacer"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/spacer"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:text="@string/connect"
+ android:id="@+id/button_connect"
+ />
+
+ <ProgressBar
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/device_spinner"
+ />
+
+ </LinearLayout>
+
+ <!--
+ <TextView
+ android:id="@+id/device_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="24sp"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="0dp"
+ android:layout_toStartOf="@+id/spacer"/>
+
+ <TextView
+ android:id="@+id/device_address"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:layout_below="@+id/device_name"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@+id/spacer"/>
+
+ <TextView
+ android:id="@+id/device_rssi"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:layout_below="@+id/device_address"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@+id/spacer"/>
+
+ <Space
+ android:id="@+id/spacer"
+ android:layout_width="2dp"
+ android:layout_height="fill_parent"
+ android:layout_toStartOf="@id/button_connect"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/connect"
+ android:id="@+id/button_connect"
+ android:layout_alignParentTop="true"
+ android:layout_toStartOf="@+id/device_spinner"
+ />
+
+ <ProgressBar
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/device_spinner"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ />
+ -->
+
+</RelativeLayout>
diff --git a/app/src/main/res/menu/gatt_services.xml b/app/src/main/res/menu/gatt_services.xml
new file mode 100644
index 0000000..25d64b6
--- /dev/null
+++ b/app/src/main/res/menu/gatt_services.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/menu_refresh"
+ android:checkable="false"
+ android:orderInCategory="1"
+ android:showAsAction="ifRoom"/>
+ <item android:id="@+id/menu_connect"
+ android:title="@string/menu_connect"
+ android:orderInCategory="100"
+ android:showAsAction="ifRoom|withText"/>
+ <item android:id="@+id/menu_disconnect"
+ android:title="@string/menu_disconnect"
+ android:orderInCategory="101"
+ android:showAsAction="ifRoom|withText"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
new file mode 100644
index 0000000..08b604e
--- /dev/null
+++ b/app/src/main/res/menu/main.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/menu_refresh"
+ android:checkable="false"
+ android:orderInCategory="1"
+ android:showAsAction="ifRoom"/>
+ <item android:id="@+id/menu_scan"
+ android:title="@string/menu_scan"
+ android:orderInCategory="100"
+ android:showAsAction="ifRoom|withText"/>
+ <item android:id="@+id/menu_stop"
+ android:title="@string/menu_stop"
+ android:orderInCategory="101"
+ android:showAsAction="ifRoom|withText"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..87a750e
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,5 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
+ <item android:id="@+id/action_settings" android:title="@string/action_settings"
+ android:orderInCategory="100" android:showAsAction="never" />
+</menu>
diff --git a/app/src/main/res/menu/menu_soil.xml b/app/src/main/res/menu/menu_soil.xml
new file mode 100644
index 0000000..df9bfbe
--- /dev/null
+++ b/app/src/main/res/menu/menu_soil.xml
@@ -0,0 +1,8 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="io.trygvis.soilmoisture.SoilActivity">
+ <item android:id="@+id/action_settings"
+ android:title="@string/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"/>
+</menu>
diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8aa5e33
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Soil Moisture</string>
+ <string name="hello_world">Hello world!</string>
+ <string name="action_settings">Settings</string>
+ <string name="ble_not_supported">BLE is not supported</string>
+ <string name="label_data">Data:</string>
+ <string name="label_device_address">Device address:</string>
+ <string name="label_state">State:</string>
+ <string name="no_data">No data</string>
+ <string name="connected">Connected</string>
+ <string name="disconnected">Disconnected</string>
+ <string name="title_devices">Soil Moisture Device Scan</string>
+ <string name="error_bluetooth_not_supported">Bluetooth not supported.</string>
+ <string name="unknown_device">Unknown device</string>
+ <string name="unknown_characteristic">Unknown characteristic</string>
+ <string name="unknown_service">Unknown service</string>
+ <string name="rssi">RSSI</string>
+ <string name="gauge_count">Gauge count</string>
+ <string name="name_unknown">Unknown name</string>
+ <string name="unknown">unknown</string>
+ <string name="connect">Connect</string>
+ <string name="menu_connect">Connect</string>
+ <string name="menu_disconnect">Disconnect</string>
+ <string name="menu_scan">Scan</string>
+ <string name="menu_stop">Stop</string>
+ <string name="disconnect">Disconnect</string>
+ <string name="title_activity_soil">SoilActivity</string>
+ <string name="title_title">Title</string>
+ <string name="title_name">Name</string>
+ <string name="refresh">Refresh</string>
+ <string name="current_value">Current value</string>
+
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ff6c9d2
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ </style>
+
+</resources>