summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorTrygve Laugstøl <trygvis@inamo.no>2014-11-30 23:55:54 +0100
committerTrygve Laugstøl <trygvis@inamo.no>2014-12-01 00:17:04 +0100
commit7a7d015f8a68f5e0d06fe6e3d9422d5f418f653d (patch)
tree35d8a59e3d5ae1e0b600d91b2ac4283380bdc896 /app
downloadfiken-display-android-7a7d015f8a68f5e0d06fe6e3d9422d5f418f653d.tar.gz
fiken-display-android-7a7d015f8a68f5e0d06fe6e3d9422d5f418f653d.tar.bz2
fiken-display-android-7a7d015f8a68f5e0d06fe6e3d9422d5f418f653d.tar.xz
fiken-display-android-7a7d015f8a68f5e0d06fe6e3d9422d5f418f653d.zip
o Initial import of Fiken Status Display app.
Diffstat (limited to 'app')
-rw-r--r--app/.gitignore1
-rw-r--r--app/app.iml86
-rw-r--r--app/build.gradle24
-rw-r--r--app/proguard-rules.pro17
-rw-r--r--app/src/androidTest/java/no/topi/fiken/display/ApplicationTest.java13
-rw-r--r--app/src/main/AndroidManifest.xml56
-rw-r--r--app/src/main/java/no/topi/fiken/display/Constants.java10
-rw-r--r--app/src/main/java/no/topi/fiken/display/DefaultDisplayService.java255
-rw-r--r--app/src/main/java/no/topi/fiken/display/DisplayControlActivity.java141
-rw-r--r--app/src/main/java/no/topi/fiken/display/DisplayService.java102
-rw-r--r--app/src/main/java/no/topi/fiken/display/ExceptionHandler.java22
-rw-r--r--app/src/main/java/no/topi/fiken/display/MainActivity.java335
-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_display_control.xml42
-rw-r--r--app/src/main/res/layout/activity_main.xml7
-rw-r--r--app/src/main/res/layout/fragment_main.xml36
-rw-r--r--app/src/main/res/layout/gatt_services_characteristics.xml71
-rw-r--r--app/src/main/res/layout/listitem_device.xml53
-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_display_control.xml8
-rw-r--r--app/src/main/res/menu/menu_main.xml5
-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.xml32
-rw-r--r--app/src/main/res/values/styles.xml8
34 files changed, 1559 insertions, 0 deletions
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/app.iml b/app/app.iml
new file mode 100644
index 0000000..6a4dade
--- /dev/null
+++ b/app/app.iml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="fiken-display-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android-gradle" name="Android-Gradle">
+ <configuration>
+ <option name="GRADLE_PROJECT_PATH" value=":app" />
+ </configuration>
+ </facet>
+ <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>
+ </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/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$/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/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/tmp" />
+ </content>
+ <orderEntry type="jdk" jdkName="Android API 18 Platform" jdkType="Android SDK" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
+
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..4073375
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,24 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 18
+ buildToolsVersion "19.1.0"
+
+ defaultConfig {
+ applicationId "no.topi.fiken.display"
+ minSdkVersion 18
+ targetSdkVersion 18
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+}
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/no/topi/fiken/display/ApplicationTest.java b/app/src/androidTest/java/no/topi/fiken/display/ApplicationTest.java
new file mode 100644
index 0000000..c306301
--- /dev/null
+++ b/app/src/androidTest/java/no/topi/fiken/display/ApplicationTest.java
@@ -0,0 +1,13 @@
+package no.topi.fiken.display;
+
+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..35956d8
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="no.topi.fiken.display" >
+
+ <!--
+ 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" />
+
+ <!--
+ <application android:label="@string/app_name"
+ android:icon="@drawable/ic_launcher"
+ android:theme="@android:style/Theme.Holo.Light">
+ <activity android:name=".DeviceScanActivity"
+ 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=".DeviceControlActivity"/>
+ <service android:name=".BluetoothLeService" android:enabled="true"/>
+ </application>
+ -->
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name="no.topi.fiken.display.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="no.topi.fiken.display.DisplayControlActivity"
+ android:label="@string/title_activity_display_control" />
+
+ <service
+ android:name=".DefaultDisplayService"
+ android:enabled="true" />
+ </application>
+
+</manifest>
diff --git a/app/src/main/java/no/topi/fiken/display/Constants.java b/app/src/main/java/no/topi/fiken/display/Constants.java
new file mode 100644
index 0000000..f568e1c
--- /dev/null
+++ b/app/src/main/java/no/topi/fiken/display/Constants.java
@@ -0,0 +1,10 @@
+package no.topi.fiken.display;
+
+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_UUID = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0002"));
+ UUID TRYGVIS_IO_LED_UUID = UUID.fromString(TRYGVIS_IO_BASE_UUID.replace("xxxx", "0003"));
+}
diff --git a/app/src/main/java/no/topi/fiken/display/DefaultDisplayService.java b/app/src/main/java/no/topi/fiken/display/DefaultDisplayService.java
new file mode 100644
index 0000000..b1aa2e7
--- /dev/null
+++ b/app/src/main/java/no/topi/fiken/display/DefaultDisplayService.java
@@ -0,0 +1,255 @@
+package no.topi.fiken.display;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+import android.widget.Toast;
+
+public class DefaultDisplayService extends Service implements DisplayService {
+ private final Context context = DefaultDisplayService.this;
+ private final static String TAG = DefaultDisplayService.class.getSimpleName();
+
+ private final IBinder binder = new LocalBinder(this);
+
+ private BluetoothManager mBluetoothManager;
+ private BluetoothAdapter mBluetoothAdapter;
+ private BluetoothGattService displayService;
+ private BluetoothGatt gatt;
+
+ private Handler handler;
+ private int UPDATE_RSSI_DELAY = 1000;
+
+ private Runnable updateRssi = new Runnable() {
+ @Override
+ public void run() {
+ if(gatt != null) {
+ gatt.readRemoteRssi();
+ }
+
+ handler.postDelayed(this, UPDATE_RSSI_DELAY);
+ }
+ };
+
+ public static enum ServiceState {
+ BROKEN,
+ IDLE,
+ SCANNING,
+ CONNECTED,
+ }
+
+ private ServiceState serviceState = ServiceState.BROKEN;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ @Override
+ public void onCreate() {
+ handler = new Handler();
+
+ handler.postDelayed(updateRssi, UPDATE_RSSI_DELAY);
+ }
+
+ public boolean initialize() {
+ // For API level 18 and above, get a reference to BluetoothAdapter through
+ // BluetoothManager.
+ if (mBluetoothManager == null) {
+ mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+ if (mBluetoothManager == null) {
+ Log.e(TAG, "Unable to initialize BluetoothManager.");
+ return false;
+ }
+ }
+
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+ if (mBluetoothAdapter == null) {
+ Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
+ return false;
+ }
+
+ Log.e(TAG, "Bluetooth initialized");
+
+ serviceState = ServiceState.IDLE;
+
+ return true;
+ }
+
+ @Override
+ public boolean connect(final String address) {
+ if (serviceState != ServiceState.IDLE) {
+ if (!(serviceState == ServiceState.CONNECTED && gatt.getDevice().getAddress().equals(address))) {
+ Log.e(TAG, "connect(): Not idle: " + serviceState);
+ return false;
+ }
+
+ Log.i(TAG, "connect(): already connected: " + serviceState);
+ return true;
+ }
+
+ BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
+
+ gatt = device.connectGatt(this, false, new BluetoothGattCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+// Toast.makeText(context, "Connected", Toast.LENGTH_SHORT).show();
+ if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) {
+ boolean ok = gatt.discoverServices();
+
+ if (!ok) {
+ disconnect();
+ } else {
+ Intent intent = IntentAction.DEVICE_UPDATE.intent();
+ intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), address);
+ intent.putExtra(IntentExtra.CONNECTED.name(), true);
+ sendBroadcast(intent);
+ }
+ } else {
+ Log.w(TAG, "Could not connect to device");
+// Toast.makeText(context, "Could not connect to device", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ Log.i(TAG, "onServicesDiscovered");
+
+ Log.i(TAG, "Constants.TRYGVIS_IO_FIKEN_STATUS_PANEL_UUID = " + Constants.TRYGVIS_IO_FIKEN_STATUS_PANEL_UUID);
+
+ for (BluetoothGattService bluetoothGattService : gatt.getServices()) {
+ Log.i(TAG, "bluetoothGattService.getUuid() = " + bluetoothGattService.getUuid());
+ }
+
+ displayService = gatt.getService(Constants.TRYGVIS_IO_FIKEN_STATUS_PANEL_UUID);
+
+ Log.i(TAG, "service=" + displayService);
+
+ Intent intent = IntentAction.DEVICE_UPDATE.intent();
+ intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), address);
+ intent.putExtra(IntentExtra.DEVICE_IS_DISPLAY.name(), displayService != null);
+ sendBroadcast(intent);
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ Log.i(TAG, "onCharacteristicRead");
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ Log.i(TAG, "onCharacteristicWrite");
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ Log.i(TAG, "onCharacteristicChanged");
+ }
+
+ @Override
+ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ Log.i(TAG, "onDescriptorRead");
+ }
+
+ @Override
+ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ Log.i(TAG, "onDescriptorWrite");
+ }
+
+ @Override
+ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+ Log.i(TAG, "onReliableWriteCompleted");
+ }
+
+ @Override
+ public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+ Log.i(TAG, "onReadRemoteRssi, status=" + status + ", rssi=" + rssi);
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ Intent intent = IntentAction.DEVICE_UPDATE.intent();
+ intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), gatt.getDevice().getAddress());
+ intent.putExtra(IntentExtra.RSSI.name(), rssi);
+ sendBroadcast(intent);
+ }
+ }
+ });
+
+ if (gatt != null) {
+ serviceState = ServiceState.CONNECTED;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void disconnect() {
+ if (serviceState != ServiceState.CONNECTED) {
+ Log.d(TAG, "disconnect(): Not connected: " + serviceState);
+ return;
+ }
+
+ serviceState = ServiceState.IDLE;
+
+ if (gatt != null) {
+ try {
+ gatt.disconnect();
+ } catch (Exception e) {
+ Log.w(TAG, "gatt.disconnect()", e);
+ }
+ try {
+ gatt.close();
+ } catch (Exception e) {
+ Log.w(TAG, "gatt.close()", e);
+ }
+ gatt = null;
+ }
+
+ displayService = null;
+ }
+
+ @Override
+ public void startScan() {
+ if (serviceState != ServiceState.IDLE) {
+ Toast.makeText(context, "startScan(): Not idle", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ serviceState = ServiceState.SCANNING;
+
+ mBluetoothAdapter.startLeScan(leScanCallback);
+ }
+
+ @Override
+ public void stopScan() {
+ Log.d(TAG, "stopScan(): stopping scanning");
+
+ if (serviceState != ServiceState.SCANNING) {
+ Log.d(TAG, "stopScan(): not scanning");
+ return;
+ }
+ mBluetoothAdapter.stopLeScan(leScanCallback);
+ serviceState = ServiceState.IDLE;
+ }
+
+ private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
+ @Override
+ public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
+ Log.i(TAG, "onLeScan()");
+ Intent intent = IntentAction.DEVICE_UPDATE.intent();
+ intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), device.getAddress());
+ intent.putExtra(IntentExtra.DEVICE_NAME.name(), device.getName());
+ intent.putExtra(IntentExtra.RSSI.name(), rssi);
+ sendBroadcast(intent);
+ }
+ };
+}
diff --git a/app/src/main/java/no/topi/fiken/display/DisplayControlActivity.java b/app/src/main/java/no/topi/fiken/display/DisplayControlActivity.java
new file mode 100644
index 0000000..1147c0c
--- /dev/null
+++ b/app/src/main/java/no/topi/fiken/display/DisplayControlActivity.java
@@ -0,0 +1,141 @@
+package no.topi.fiken.display;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.Menu;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import static java.lang.String.valueOf;
+import static no.topi.fiken.display.DisplayService.DeviceInfo;
+import static no.topi.fiken.display.DisplayService.IntentAction;
+import static no.topi.fiken.display.DisplayService.IntentExtra;
+import static no.topi.fiken.display.DisplayService.LocalBinder;
+
+public class DisplayControlActivity extends Activity {
+ private final static String TAG = DisplayControlActivity.class.getSimpleName();
+
+ private DisplayService displayService;
+ private DeviceInfo deviceInfo;
+
+ private TextView deviceNameView;
+ private TextView deviceRssiView;
+ private LinearLayout gaugesLayout;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_display_control);
+
+ final Intent intent = getIntent();
+ String deviceAddress = intent.getStringExtra(IntentExtra.DEVICE_ADDRESS.name());
+ deviceInfo = new DeviceInfo(deviceAddress, 0);
+ deviceInfo.name = intent.getStringExtra(IntentExtra.DEVICE_NAME.name());
+
+ Intent displayServiceIntent = new Intent(this, DefaultDisplayService.class);
+ bindService(displayServiceIntent, serviceConnection, BIND_AUTO_CREATE);
+
+ deviceNameView = (TextView) findViewById(R.id.device_name);
+ deviceRssiView = (TextView) findViewById(R.id.device_rssi);
+ gaugesLayout = (LinearLayout) findViewById(R.id.gauges);
+
+ Button disconnectButton = (Button) findViewById(R.id.button_disconnect);
+ disconnectButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ disconnect();
+ }
+ });
+
+ updateValues();
+ }
+
+ private void updateValues() {
+ deviceNameView.setText(deviceInfo.name != null ? deviceInfo.name : getText(R.string.name_unknown));
+ deviceRssiView.setText(getText(R.string.rssi) + ": " + (deviceInfo.rssi != 0 ? valueOf(deviceInfo.rssi) : ""));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ registerReceiver(displayServiceBroadcastReceiver, IntentAction.ALL_FILTER);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ unregisterReceiver(displayServiceBroadcastReceiver);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ displayService.disconnect();
+ unbindService(serviceConnection);
+ displayService = null;
+ }
+
+ @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_display_control, menu);
+ return true;
+ }
+
+ public void disconnect() {
+ if (displayService != null) {
+ displayService.disconnect();
+ }
+
+ finish();
+ }
+
+ private final ServiceConnection serviceConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ displayService = ((LocalBinder) service).getService();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ displayService = null;
+ }
+ };
+
+ private final BroadcastReceiver displayServiceBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+
+ IntentAction action = IntentAction.valueOf(intent);
+
+ String deviceAddress = intent.getStringExtra(IntentExtra.DEVICE_ADDRESS.name());
+
+ if (action == IntentAction.DEVICE_UPDATE && deviceInfo.address.equals(deviceAddress)) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ deviceInfo.update(intent);
+
+ if (intent.hasExtra(IntentExtra.CONNECTED.name())) {
+ boolean connected = intent.getBooleanExtra(IntentExtra.CONNECTED.name(), false);
+
+ if (!connected) {
+ finish();
+ }
+ }
+ updateValues();
+ }
+ });
+ }
+ }
+ };
+}
diff --git a/app/src/main/java/no/topi/fiken/display/DisplayService.java b/app/src/main/java/no/topi/fiken/display/DisplayService.java
new file mode 100644
index 0000000..c907138
--- /dev/null
+++ b/app/src/main/java/no/topi/fiken/display/DisplayService.java
@@ -0,0 +1,102 @@
+package no.topi.fiken.display;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Binder;
+
+public interface DisplayService {
+
+ enum IntentExtra {
+ DEVICE_NAME,
+ DEVICE_ADDRESS,
+ DEVICE_IS_DISPLAY,
+ RSSI,
+ SCANNING,
+ CONNECTED,
+ }
+
+ public static enum IntentAction {
+ DEVICE_UPDATE;
+
+ private final String key = getClass().getName() + "." + name();
+
+ public Intent intent() {
+ return new Intent(key);
+ }
+
+ public static final IntentFilter ALL_FILTER = new IntentFilter() {{
+ for (DefaultDisplayService.IntentAction intentAction : DefaultDisplayService.IntentAction.values()) {
+ addAction(intentAction.key);
+ }
+ }};
+
+ public static IntentAction valueOf(Intent intent) {
+ try {
+ return valueOf(intent.getAction().replaceAll(".*\\.", ""));
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+ }
+
+ public class LocalBinder extends Binder {
+ private final DisplayService service;
+
+ public LocalBinder(DisplayService service) {
+ this.service = service;
+ }
+
+ DisplayService getService() {
+ return service;
+ }
+ }
+
+ boolean initialize();
+
+ void startScan();
+
+ void stopScan();
+
+ boolean connect(String address);
+
+ public void disconnect();
+
+ class DeviceInfo {
+ final String address;
+ int rssi = 0;
+ Boolean isDisplay = null;
+ String name;
+
+ DeviceInfo(String address, int rssi) {
+ this.address = address;
+ this.rssi = rssi;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DeviceInfo that = (DeviceInfo) o;
+
+ return address.equals(that.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return address.hashCode();
+ }
+
+ public void update(Intent intent) {
+ if (intent.hasExtra(IntentExtra.DEVICE_IS_DISPLAY.name())) {
+ isDisplay = intent.getBooleanExtra(IntentExtra.DEVICE_IS_DISPLAY.name(), false);
+ }
+ if (intent.hasExtra(IntentExtra.RSSI.name())) {
+ rssi = intent.getIntExtra(IntentExtra.RSSI.name(), 0);
+ }
+ if (intent.hasExtra(IntentExtra.DEVICE_NAME.name())) {
+ name = intent.getStringExtra(IntentExtra.DEVICE_NAME.name());
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/no/topi/fiken/display/ExceptionHandler.java b/app/src/main/java/no/topi/fiken/display/ExceptionHandler.java
new file mode 100644
index 0000000..3d4560b
--- /dev/null
+++ b/app/src/main/java/no/topi/fiken/display/ExceptionHandler.java
@@ -0,0 +1,22 @@
+package no.topi.fiken.display;
+
+import android.util.Log;
+
+public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
+ private final static String TAG = DefaultDisplayService.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/no/topi/fiken/display/MainActivity.java b/app/src/main/java/no/topi/fiken/display/MainActivity.java
new file mode 100644
index 0000000..d9dc073
--- /dev/null
+++ b/app/src/main/java/no/topi/fiken/display/MainActivity.java
@@ -0,0 +1,335 @@
+package no.topi.fiken.display;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.ListActivity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Handler;
+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.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.lang.String.valueOf;
+import static no.topi.fiken.display.DisplayService.IntentAction;
+import static no.topi.fiken.display.DisplayService.IntentExtra;
+import static no.topi.fiken.display.ExceptionHandler.EXCEPTION_HANDLER;
+
+public class MainActivity extends ListActivity {
+ private final static String TAG = MainActivity.class.getSimpleName();
+
+ // Stops scanning after 10 seconds.
+ private static final long SCAN_PERIOD = 3 * 1000;
+
+ private static final int REQUEST_ENABLE_BT = 1;
+
+ private DisplayListAdapter displayList;
+ private Handler handler;
+ private BluetoothAdapter mBluetoothAdapter;
+ private boolean mScanning;
+ private DisplayService displayService;
+ private String deviceToShow;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ Thread.setDefaultUncaughtExceptionHandler(EXCEPTION_HANDLER);
+ super.onCreate(savedInstanceState);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.title_devices);
+ }
+ handler = new Handler();
+
+ // 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();
+ finish();
+ }
+
+ // Initializes a Bluetooth adapter. For API level 18 and above, get a reference to
+ // BluetoothAdapter through BluetoothManager.
+ final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+ mBluetoothAdapter = bluetoothManager.getAdapter();
+
+ // Checks if Bluetooth is supported on the device.
+ if (mBluetoothAdapter == null) {
+ Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ ServiceConnection serviceConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ displayService = ((DisplayService.LocalBinder) service).getService();
+ if (!displayService.initialize()) {
+ finish();
+ }
+
+ startScan();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ stopScan();
+ displayService = null;
+ }
+ };
+
+ Intent displayServiceIntent = new Intent(this, DefaultDisplayService.class);
+ bindService(displayServiceIntent, serviceConnection, BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void onResume() {
+ Log.i(TAG, "onResume");
+
+ super.onResume();
+
+ // 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.isEnabled()) {
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
+ }
+
+ registerReceiver(displayServiceBroadcastReceiver, IntentAction.ALL_FILTER);
+ }
+
+ @Override
+ protected void onPause() {
+ Log.i(TAG, "onPause");
+
+ super.onPause();
+ stopScan();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.i(TAG, "onActivityResult");
+
+ // User chose not to enable Bluetooth.
+ if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) {
+ finish();
+ return;
+ }
+
+ 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 (!mScanning) {
+ 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() {
+ displayList = new DisplayListAdapter();
+ setListAdapter(displayList);
+
+ displayService.startScan();
+
+ // Stops scanning after a pre-defined scan period.
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ displayService.stopScan();
+ invalidateOptionsMenu();
+ }
+ }, SCAN_PERIOD);
+ }
+
+ private void stopScan() {
+ if (displayService != null) {
+ displayService.stopScan();
+ }
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ stopScan();
+
+ DisplayService.DeviceInfo state = displayList.getDevice(position);
+
+ if (!displayService.connect(state.address)) {
+ Toast.makeText(this, "Could not connect to " + state.address, Toast.LENGTH_SHORT).show();
+ } else {
+ deviceToShow = state.address;
+ }
+ }
+
+ private final BroadcastReceiver displayServiceBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ IntentAction action = IntentAction.valueOf(intent);
+
+ String deviceAddress = intent.getStringExtra(IntentExtra.DEVICE_ADDRESS.name());
+
+ if (action == IntentAction.DEVICE_UPDATE && deviceAddress != null) {
+ DisplayService.DeviceInfo device = displayList.getDevice(deviceAddress, true);
+
+ device.update(intent);
+
+ if (intent.hasExtra(IntentExtra.CONNECTED.name())) {
+ boolean connected = intent.getBooleanExtra(IntentExtra.CONNECTED.name(), false);
+
+ if (connected) {
+ if (deviceToShow != null && deviceToShow.equals(device.address)) {
+ Log.i(TAG, "connected to " + deviceToShow);
+ final Intent intent = new Intent(context, DisplayControlActivity.class);
+ intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), device.address);
+ intent.putExtra(IntentExtra.DEVICE_NAME.name(), device.name);
+ startActivity(intent);
+ }
+ }
+ }
+ displayList.notifyDataSetChanged();
+ }
+
+ if (intent.hasExtra(IntentExtra.SCANNING.name())) {
+ mScanning = intent.getBooleanExtra(IntentExtra.SCANNING.name(), false);
+ invalidateOptionsMenu();
+ }
+ }
+ });
+ }
+ };
+
+ static class ViewHolder {
+ final TextView deviceName;
+ final TextView deviceAddress;
+ final TextView rssi;
+ final TextView isDisplay;
+
+ ViewHolder(TextView deviceName, TextView deviceAddress, TextView rssi, TextView isDisplay) {
+ this.deviceName = deviceName;
+ this.deviceAddress = deviceAddress;
+ this.rssi = rssi;
+ this.isDisplay = isDisplay;
+ }
+ }
+
+ private class DisplayListAdapter extends BaseAdapter {
+ private List<DisplayService.DeviceInfo> devices = new ArrayList<DisplayService.DeviceInfo>();
+ private LayoutInflater inflater = MainActivity.this.getLayoutInflater();
+
+ public DisplayService.DeviceInfo getDevice(int position) {
+ return devices.get(position);
+ }
+
+ public DisplayService.DeviceInfo getDevice(String address, boolean create) {
+ for (DisplayService.DeviceInfo device : devices) {
+ if (device.address.equals(address)) {
+ return device;
+ }
+ }
+
+ DisplayService.DeviceInfo deviceInfo = null;
+ if (create) {
+ deviceInfo = new DisplayService.DeviceInfo(address, 0);
+ devices.add(deviceInfo);
+ }
+ return deviceInfo;
+ }
+
+ @Override
+ public int getCount() {
+ return devices.size();
+ }
+
+ @Override
+ public Object getItem(int i) {
+ return devices.get(i);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public View getView(int i, View view, ViewGroup viewGroup) {
+ ViewHolder viewHolder;
+
+ if (view == null) {
+ view = inflater.inflate(R.layout.listitem_device, null);
+ viewHolder = new ViewHolder(
+ (TextView) view.findViewById(R.id.device_name),
+ (TextView) view.findViewById(R.id.device_address),
+ (TextView) view.findViewById(R.id.device_rssi),
+ (TextView) view.findViewById(R.id.device_isDisplay));
+ view.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+
+ DisplayService.DeviceInfo state = devices.get(i);
+ if (state.name != null && state.name.length() > 0) {
+ viewHolder.deviceName.setText(state.name);
+ } else {
+ viewHolder.deviceName.setText(R.string.unknown_device);
+ }
+ viewHolder.deviceAddress.setText(state.address);
+
+ viewHolder.rssi.setText(getText(R.string.rssi) + ": " +
+ (state.rssi != 0 ? valueOf(state.rssi) : getText(R.string.rssi_unknown)));
+
+ viewHolder.isDisplay.setText("Is display: " +
+ (state.isDisplay != null ? state.isDisplay : "unknown"));
+ view.setClickable(state.isDisplay != null && state.isDisplay);
+
+ return view;
+ }
+ }
+}
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_display_control.xml b/app/src/main/res/layout/activity_display_control.xml
new file mode 100644
index 0000000..4151184
--- /dev/null
+++ b/app/src/main/res/layout/activity_display_control.xml
@@ -0,0 +1,42 @@
+<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="no.topi.fiken.display.DisplayControlActivity">
+
+ <TextView
+ android:id="@+id/device_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="24sp"
+ android:layout_alignParentEnd="true"/>
+
+ <TextView
+ android:id="@+id/device_rssi"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:layout_below="@+id/device_name"/>
+
+ <Button
+ android:id="@+id/button_disconnect"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/disconnect"
+ android:layout_below="@+id/device_rssi"
+ />
+
+ <LinearLayout
+ android:id="@+id/gauges"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_below="@+id/button_disconnect"
+ android:layout_alignParentStart="true">
+ </LinearLayout>
+
+</RelativeLayout>
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/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
new file mode 100644
index 0000000..8958013
--- /dev/null
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,36 @@
+<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=".MainActivity$PlaceholderFragment">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="New Button"
+ android:id="@+id/button"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true" />
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/progressBar"
+ android:layout_centerVertical="true"
+ android:layout_centerHorizontal="true"
+ android:visibility="gone" />
+
+ <ListView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/listView"
+ android:layout_toStartOf="@+id/progressBar"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:layout_above="@+id/button"
+ android:layout_alignParentEnd="true" />
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/gatt_services_characteristics.xml b/app/src/main/res/layout/gatt_services_characteristics.xml
new file mode 100644
index 0000000..0156afd
--- /dev/null
+++ b/app/src/main/res/layout/gatt_services_characteristics.xml
@@ -0,0 +1,71 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp">
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp">
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_device_address"
+ android:textSize="18sp"/>
+ <Space android:layout_width="5dp"
+ android:layout_height="wrap_content"/>
+ <TextView android:id="@+id/device_address"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"/>
+ </LinearLayout>
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp">
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_state"
+ android:textSize="18sp"/>
+ <Space android:layout_width="5dp"
+ android:layout_height="wrap_content"/>
+ <TextView android:id="@+id/connection_state"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/disconnected"
+ android:textSize="18sp"/>
+ </LinearLayout>
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp">
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_data"
+ android:textSize="18sp"/>
+ <Space android:layout_width="5dp"
+ android:layout_height="wrap_content"/>
+ <TextView android:id="@+id/data_value"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/no_data"
+ android:textSize="18sp"/>
+ </LinearLayout>
+ <ExpandableListView android:id="@+id/gatt_services_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
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..54f529e
--- /dev/null
+++ b/app/src/main/res/layout/listitem_device.xml
@@ -0,0 +1,53 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/device_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="24sp"/>
+
+ <TextView
+ android:id="@+id/device_address"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"/>
+
+ <TextView
+ android:id="@+id/device_rssi"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"/>
+
+ <TextView
+ android:id="@+id/device_isDisplay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"/>
+
+<!--
+ <Button
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/connect"
+ android:id="@+id/button2"/>
+-->
+
+</LinearLayout>
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_display_control.xml b/app/src/main/res/menu/menu_display_control.xml
new file mode 100644
index 0000000..51a408b
--- /dev/null
+++ b/app/src/main/res/menu/menu_display_control.xml
@@ -0,0 +1,8 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="no.topi.fiken.display.DisplayControlActivity">
+ <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_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/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..0cea3b4
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Display</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">BLE 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="name_unknown">Unknown name</string>
+ <string name="rssi_unknown">unknown</string>
+ <string name="connect">Connect</string>
+
+ <!-- Menu items -->
+ <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="title_activity_display_control">DisplayControlActivity</string>
+ <string name="disconnect">Disconnect</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>