From 7a7d015f8a68f5e0d06fe6e3d9422d5f418f653d Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sun, 30 Nov 2014 23:55:54 +0100 Subject: o Initial import of Fiken Status Display app. --- .gitignore | 6 + .idea/.name | 1 + .idea/codeStyleSettings.xml | 174 +++++++++++ .idea/compiler.xml | 23 ++ .idea/copyright/profiles_settings.xml | 3 + .idea/dictionaries/trygvis.xml | 10 + .idea/encodings.xml | 5 + .idea/gradle.xml | 18 ++ .idea/misc.xml | 10 + .idea/modules.xml | 10 + .idea/scopes/scope_settings.xml | 5 + .idea/vcs.xml | 7 + app/.gitignore | 1 + app/app.iml | 86 ++++++ app/build.gradle | 24 ++ app/proguard-rules.pro | 17 ++ .../no/topi/fiken/display/ApplicationTest.java | 13 + app/src/main/AndroidManifest.xml | 56 ++++ .../main/java/no/topi/fiken/display/Constants.java | 10 + .../topi/fiken/display/DefaultDisplayService.java | 255 ++++++++++++++++ .../topi/fiken/display/DisplayControlActivity.java | 141 +++++++++ .../java/no/topi/fiken/display/DisplayService.java | 102 +++++++ .../no/topi/fiken/display/ExceptionHandler.java | 22 ++ .../java/no/topi/fiken/display/MainActivity.java | 335 +++++++++++++++++++++ app/src/main/res/demo/demo/activity_main2.xml | 36 +++ app/src/main/res/demo/values/base-strings.xml | 31 ++ app/src/main/res/demo/values/template-dimens.xml | 32 ++ app/src/main/res/demo/values/template-styles.xml | 44 +++ app/src/main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 9397 bytes app/src/main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes app/src/main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes app/src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 19388 bytes .../layout/actionbar_indeterminate_progress.xml | 23 ++ .../main/res/layout/activity_display_control.xml | 42 +++ app/src/main/res/layout/activity_main.xml | 7 + app/src/main/res/layout/fragment_main.xml | 36 +++ .../res/layout/gatt_services_characteristics.xml | 71 +++++ app/src/main/res/layout/listitem_device.xml | 53 ++++ app/src/main/res/menu/gatt_services.xml | 29 ++ app/src/main/res/menu/main.xml | 29 ++ app/src/main/res/menu/menu_display_control.xml | 8 + app/src/main/res/menu/menu_main.xml | 5 + app/src/main/res/values-w820dp/dimens.xml | 6 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 32 ++ app/src/main/res/values/styles.xml | 8 + build.gradle | 19 ++ fiken-display-android.iml | 19 ++ gradle.properties | 18 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++++++++++ gradlew.bat | 90 ++++++ settings.gradle | 1 + 54 files changed, 2148 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/codeStyleSettings.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/dictionaries/trygvis.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/app.iml create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/no/topi/fiken/display/ApplicationTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/no/topi/fiken/display/Constants.java create mode 100644 app/src/main/java/no/topi/fiken/display/DefaultDisplayService.java create mode 100644 app/src/main/java/no/topi/fiken/display/DisplayControlActivity.java create mode 100644 app/src/main/java/no/topi/fiken/display/DisplayService.java create mode 100644 app/src/main/java/no/topi/fiken/display/ExceptionHandler.java create mode 100644 app/src/main/java/no/topi/fiken/display/MainActivity.java create mode 100644 app/src/main/res/demo/demo/activity_main2.xml create mode 100644 app/src/main/res/demo/values/base-strings.xml create mode 100644 app/src/main/res/demo/values/template-dimens.xml create mode 100644 app/src/main/res/demo/values/template-styles.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/layout/actionbar_indeterminate_progress.xml create mode 100644 app/src/main/res/layout/activity_display_control.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/fragment_main.xml create mode 100644 app/src/main/res/layout/gatt_services_characteristics.xml create mode 100644 app/src/main/res/layout/listitem_device.xml create mode 100644 app/src/main/res/menu/gatt_services.xml create mode 100644 app/src/main/res/menu/main.xml create mode 100644 app/src/main/res/menu/menu_display_control.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 build.gradle create mode 100644 fiken-display-android.iml create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..afbdab3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..7d5a8b0 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Display \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..29ab470 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,174 @@ + + + + + + + diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..217af47 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/dictionaries/trygvis.xml b/.idea/dictionaries/trygvis.xml new file mode 100644 index 0000000..9afac00 --- /dev/null +++ b/.idea/dictionaries/trygvis.xml @@ -0,0 +1,10 @@ + + + + fiken + rssi + trygvis + xxxx + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..736c7b5 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2497215 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..342f96e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..275077f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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 devices = new ArrayList(); + 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 @@ + + + + + + + + + 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 @@ + + + + + + + + \ 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 @@ + + + + + + + 4dp + 8dp + 16dp + 32dp + 64dp + + + + @dimen/margin_medium + @dimen/margin_medium + + \ 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 @@ + + + + + + + + + + + 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png 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 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png 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 @@ + + + + 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 @@ + + + + + + +