diff options
Diffstat (limited to 'app')
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 Binary files differnew file mode 100644 index 0000000..96a442e --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..359047d --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..71c6d76 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..4df1894 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_launcher.png 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> |