From 29b35d48bc2a6817a9d9570492eb9d842f07a67c Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Thu, 16 Apr 2015 16:05:42 +0200 Subject: [PATCH] Template profile added --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 24 ++- .../nrftoolbox/parser/TemplateParser.java | 61 ++++++ .../android/nrftoolbox/template/Readme.txt | 76 ++++++++ .../nrftoolbox/template/TemplateActivity.java | 163 ++++++++++++++++ .../nrftoolbox/template/TemplateManager.java | 136 ++++++++++++++ .../template/TemplateManagerCallbacks.java | 41 ++++ .../nrftoolbox/template/TemplateService.java | 177 ++++++++++++++++++ .../template/settings/SettingsActivity.java | 50 +++++ .../template/settings/SettingsFragment.java | 42 +++++ .../drawable-hdpi/ic_stat_notify_template.png | Bin 0 -> 546 bytes .../res/drawable-hdpi/ic_template_feature.png | Bin 0 -> 2597 bytes .../ic_stat_notify_template.png | Bin 0 -> 658 bytes .../drawable-xhdpi/ic_template_feature.png | Bin 0 -> 3771 bytes .../res/layout/activity_feature_template.xml | 136 ++++++++++++++ app/src/main/res/values/strings_template.xml | 52 +++++ app/src/main/res/xml/settings_hts.xml | 4 +- app/src/main/res/xml/settings_template.xml | 34 ++++ 18 files changed, 994 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemplateParser.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java create mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_template.png create mode 100644 app/src/main/res/drawable-hdpi/ic_template_feature.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_template.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_template_feature.png create mode 100644 app/src/main/res/layout/activity_feature_template.xml create mode 100644 app/src/main/res/values/strings_template.xml create mode 100644 app/src/main/res/xml/settings_template.xml diff --git a/app/build.gradle b/app/build.gradle index fb29324d4..d47ddaf30 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "no.nordicsemi.android.nrftoolbox" minSdkVersion 18 targetSdkVersion 22 - versionCode 30 - versionName "1.12.1" + versionCode 31 + versionName "1.13.0" } buildTypes { release { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index acf0f856e..4383534bf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,8 +23,8 @@ + android:versionCode="31" + android:versionName="1.13.0" > + + + + + + + + + + + + + 0; + + // heart rate value is 8 or 16 bit long + int value = characteristic.getIntValue(value16bit ? BluetoothGattCharacteristic.FORMAT_UINT16 : BluetoothGattCharacteristic.FORMAT_UINT8, offset++); // bits per minute + if (value16bit) + offset++; + + // TODO parse more data + + final StringBuilder builder = new StringBuilder(); + builder.append("Template Measurement: ").append(value).append(" bpm"); + return builder.toString(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt new file mode 100644 index 000000000..aa3ffa43a --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + nRF Toolbox demonstrates how to implement the Bluetooth Smart features in an Android application. + It consists of number of profiles, like Heart Rate, Blood Pressure etc. that may be use as is to communicate with real devices. + They use the Bluetooth SIG adopted profiles, that may be found here: https://developer.bluetooth.org/gatt/profiles/Pages/ProfilesHome.aspx + + The Template Profile has been created to give a quick start with implementing proprietary services. Just start modifying 4 classes inside + the template package to implement features you need. + + Below you will find a short step-by-step tutorial: + + 1. The template consists of the following files: + - TemplateActivity - the main class that is responsible for managing the view of your profile + - TemplateService - the service that is started whenever you connect to a device. I handles the Bluetooth Smart communication using the... + - TemplateManager - the manager that handles all the BLE logic required to communicate with the device. The TemplateManager derives from + the BleManager which handles most of the event itself and propagates the data-relevant to deriving class. You don't + have to, or even shouldn't modify the BleManager (unless you want to change the default behaviour). + - TemplateManagerCallbacks - the interface with a list of callbacks that the TemplateManager can call. Each method is usually related to one + BLE event, e.g. receiving a new value of the characteristic.\ + - TemplateParser - an optional class in the .parser package that is responsible for converting the characteristic value to String. + This is used only for debugging. The String returned by the parse(..) method is then logged into the nRF Logger application + (if such installed). + - /settings/SettingsActivity and /settings/SettingsFragment - classes used to present user preferences. A stub implementation in the template. + - /res/layout/activity_feature_template.xml - the layout file for the TemplateActivity + - /res/values/strings_template.xml - a set of strings used in the layout file + - /res/xml/settings/template.xml - the user settings configuration + - /res/drawable/(x)hdpi/ic_template_feature.png - the template profile icon (HDPI, XHDPI). Please, keep the files size. + - /res/drawable/(x)hdpi/ic_stat_notify_template - the icon that is used in the notification + +2. The communication between the components goes as follows: + - User clicks the CONNECT button and selects a target device on TemplateActivity. + - The base class of the TemplateActivity starts the service given by getServiceClass() method. + - The service starts and initializes the TemplateManager. TemplateActivity binds to the service and is being given the TemplateBinder object (the service API) as a result. + - The manager connects to the device using Bluetooth Smart and discovers its services. + - The manager initializes the device. Initialization is done using the list of actions given by the initGatt(..) method in the TemplateManager. + Initialization usually contains enabling notifications, writing some initial values etc. + - When initialization is complete the manager calls the onDeviceReady() callback. + - The service sends the BROADCAST_DEVICE_READY broadcast to the activity. Communication from the Service to the Activity is always done using the LocalBroadcastManager broadcasts. + - The base class of the TemplateActivity listens to the broadcasts and calls appropriate methods. + + - When a custom event occurs, for example a notification is received, the manager parses the incoming data and calls the proper callback. + - The callback implementation in the TemplateService sends a broadcast message with values given in parameters. + - The TemplateActivity, which had registered a broadcast receiver before, listens to the broadcasts, reads the values and present them to users. + + - Communication Activity->Service is done using the API in the TemplateBinder. You may find the example of how to use it in the ProximityActivity. + +3. Please read the files listed above and the TODO messages for more information what to modify in the files. + +4. Remember to add your activities and the service in the AndroidManifest.xml file. The nRF Toolbox lists all activities with the following intent filter: + + + + + +5. Feel free to rename the nRF Toolbox application (/res/values/strings.xml ->app_name), change the toolbar colors (/res/values/color.xml -> actionBarColor, actionBarColorDark). + In order to remove unused profiles from the main FeaturesActivity just comment out their intent-filter tags in the AndroidManifest.xml file. diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java new file mode 100644 index 000000000..e7197cd60 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrftoolbox.template; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.view.Menu; +import android.widget.TextView; + +import java.util.UUID; + +import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; +import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; +import no.nordicsemi.android.nrftoolbox.template.settings.SettingsActivity; + +/** + * Modify the Template Activity to match your needs. + */ +public class TemplateActivity extends BleProfileServiceReadyActivity { + @SuppressWarnings("unused") + private final String TAG = "TemplateActivity"; + + // TODO change view references to match your need + private TextView mValueView; + private TextView mValueUnitView; + + @Override + protected void onCreateView(final Bundle savedInstanceState) { + // TODO modify the layout file(s). By default the activity shows only one field - the Heart Rate value as a sample + setContentView(R.layout.activity_feature_template); + setGUI(); + } + + private void setGUI() { + // TODO assign your views to fields + mValueView = (TextView) findViewById(R.id.value); + mValueUnitView = (TextView) findViewById(R.id.value_unit); + } + + @Override + protected void onInitialize(final Bundle savedInstanceState) { + LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, makeIntentFilter()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver); + } + + @Override + protected void setDefaultUI() { + // TODO clear your UI + mValueView.setText(R.string.not_available_value); + } + + @Override + protected void onServiceBinded(final TemplateService.TemplateBinder binder) { + // not used + } + + @Override + protected void onServiceUnbinded() { + // not used + } + + @Override + protected int getLoggerProfileTitle() { + return R.string.template_feature_title; + } + + @Override + protected int getAboutTextId() { + return R.string.template_about_text; + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.settings_and_about, menu); + return true; + } + + @Override + protected boolean onOptionsItemSelected(final int itemId) { + switch (itemId) { + case R.id.action_settings: + final Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + break; + } + return true; + } + + @Override + protected int getDefaultDeviceName() { + return R.string.template_default_name; + } + + @Override + protected UUID getFilterUUID() { + // TODO this method may return the UUID of the service that is required to be in the advertisement packet of a device in order to be listed on the Scanner dialog. + // If null is returned no filtering is done. + return TemplateManager.SERVICE_UUID; + } + + @Override + protected Class getServiceClass() { + return TemplateService.class; + } + + @Override + public void onServicesDiscovered(boolean optionalServicesFound) { + // this may notify user or show some views + } + + private void setValueOnView(final int value) { + // TODO assign the value to a view + mValueView.setText(String.valueOf(value)); + } + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final String action = intent.getAction(); + + if (TemplateService.BROADCAST_TEMPLATE_MEASUREMENT.equals(action)) { + final int value = intent.getIntExtra(TemplateService.EXTRA_DATA, 0); + // Update GUI + setValueOnView(value); + } + } + }; + + private static IntentFilter makeIntentFilter() { + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(TemplateService.BROADCAST_TEMPLATE_MEASUREMENT); + return intentFilter; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java new file mode 100644 index 000000000..1c731c784 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrftoolbox.template; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.UUID; + +import no.nordicsemi.android.log.Logger; +import no.nordicsemi.android.nrftoolbox.parser.TemplateParser; +import no.nordicsemi.android.nrftoolbox.profile.BleManager; + +/** + * Modify to template manager to match your requirements. + */ +public class TemplateManager extends BleManager { + private static final String TAG = "TemplateManager"; + + /** The service UUID */ + public final static UUID SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb"); // TODO change the UUID to your match your service + /** The characteristic UUID */ + private static final UUID MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb"); // TODO change the UUID to your match your characteristic + + // TODO add more services and characteristics, if required + private BluetoothGattCharacteristic mCharacteristic; + + public TemplateManager(final Context context) { + super(context); + } + + @Override + protected BleManagerGattCallback getGattCallback() { + return mGattCallback; + } + + /** + * BluetoothGatt callbacks for connection/disconnection, service discovery, receiving indication, etc + */ + private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() { + + @Override + protected Queue initGatt(final BluetoothGatt gatt) { + final LinkedList requests = new LinkedList<>(); + // TODO initialize your device, enable required notifications and indications, write what needs to be written to start working + requests.push(Request.newEnableNotificationsRequest(mCharacteristic)); + return requests; + } + + @Override + protected boolean isRequiredServiceSupported(final BluetoothGatt gatt) { + final BluetoothGattService service = gatt.getService(SERVICE_UUID); + if (service != null) { + mCharacteristic = service.getCharacteristic(MEASUREMENT_CHARACTERISTIC_UUID); + } + return mCharacteristic != null; + } + + @Override + protected void onDeviceDisconnected() { + mCharacteristic = null; + } + + // TODO implement data handlers. Methods below are called after the initialization is complete. + + @Override + protected void onDeviceReady() { + super.onDeviceReady(); + + // TODO initialization is now ready. The activity is being notified using TemplateManagerCallbacks#onDeviceReady() method. + // This method may be removed from this class if not required as the super class implementation handles this event. + } + + @Override + protected void onCharacteristicNotified(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + // TODO this method is called when a notification has been received + // This method may be removed from this class if not required + + if (mLogSession != null) + Logger.a(mLogSession, TemplateParser.parse(characteristic)); + + int value; + final int flags = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + if ((flags & 0x01) > 0) { + value = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1); + } else { + value = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1); + } + //This will send callback to the Activity when new value is received from HR device + mCallbacks.onSampleValueReceived(value); + } + + @Override + protected void onCharacteristicIndicated(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + // TODO this method is called when an indication has been received + // This method may be removed from this class if not required + } + + @Override + protected void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + // TODO this method is called when the characteristic has been read + // This method may be removed from this class if not required + } + + @Override + protected void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + // TODO this method is called when the characteristic has been written + // This method may be removed from this class if not required + } + }; + + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java new file mode 100644 index 000000000..125c25f51 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrftoolbox.template; + +import no.nordicsemi.android.nrftoolbox.profile.BleManagerCallbacks; + +/** + * Interface {@link TemplateManagerCallbacks} must be implemented by {@link TemplateActivity} in order to receive callbacks from {@link TemplateManager} + */ +public interface TemplateManagerCallbacks extends BleManagerCallbacks { + + // TODO add more callbacks. Callbacks are called when a data has been received/written to a remote device. This is the way how the manager notifies the activity about this event. + + /** + * Called when a value is received. + * + * @param value + * the new value + */ + public void onSampleValueReceived(int value); + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java new file mode 100644 index 000000000..6b0ba5d4a --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.template; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.v4.content.LocalBroadcastManager; + +import no.nordicsemi.android.log.Logger; +import no.nordicsemi.android.nrftoolbox.FeaturesActivity; +import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.profile.BleManager; +import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; + +public class TemplateService extends BleProfileService implements TemplateManagerCallbacks { + public static final String BROADCAST_TEMPLATE_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.template.BROADCAST_MEASUREMENT"; + public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.template.EXTRA_DATA"; + + private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.template.ACTION_DISCONNECT"; + + private final static int NOTIFICATION_ID = 864; + private final static int OPEN_ACTIVITY_REQ = 0; + private final static int DISCONNECT_REQ = 1; + + private TemplateManager mManager; + + private final LocalBinder mBinder = new TemplateBinder(); + + /** + * This local binder is an interface for the bonded activity to operate with the HTS sensor + */ + public class TemplateBinder extends LocalBinder { + // empty + // TODO add some service API that mey be used from the Activity + + // public void setLights(final boolean on) { + // Logger.v(getLogSession(), "Light set to: " + on); + // mManager.setLights(on); + // } + } + + @Override + protected LocalBinder getBinder() { + return mBinder; + } + + @Override + protected BleManager initializeManager() { + return mManager = new TemplateManager(this); + } + + @Override + public void onCreate() { + super.onCreate(); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DISCONNECT); + registerReceiver(mDisconnectActionBroadcastReceiver, filter); + } + + @Override + public void onDestroy() { + // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService + cancelNotification(); + unregisterReceiver(mDisconnectActionBroadcastReceiver); + + super.onDestroy(); + } + + @Override + protected void onRebind() { + // when the activity rebinds to the service, remove the notification + cancelNotification(); + } + + @Override + protected void onUnbind() { + // when the activity closes we need to show the notification that user is connected to the sensor + createNotification(R.string.template_notification_connected_message, 0); + } + + @Override + protected void onServiceStarted() { + // logger is now available. Assign it to the manager + mManager.setLogger(getLogSession()); + } + + @Override + public void onSampleValueReceived(final int value) { + final Intent broadcast = new Intent(BROADCAST_TEMPLATE_MEASUREMENT); + broadcast.putExtra(EXTRA_DATA, value); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + + if (!mBinded) { + // Here we may update the notification to display the current value. + // TODO modify the notification here + } + } + + /** + * Creates the notification + * + * @param messageResId + * message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults + * signals that will be used to notify the user + */ + private void createNotification(final int messageResId, final int defaults) { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, TemplateActivity.class); + + final Intent disconnect = new Intent(ACTION_DISCONNECT); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + + // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed + final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); + final Notification.Builder builder = new Notification.Builder(this).setContentIntent(pendingIntent); + builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); + builder.setSmallIcon(R.drawable.ic_stat_notify_template); + builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); + builder.addAction(R.drawable.ic_action_bluetooth, getString(R.string.template_notification_action_disconnect), disconnectAction); + + final Notification notification = builder.build(); + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotification() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + } + + /** + * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. + */ + private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); + if (isConnected()) + getBinder().disconnect(); + else + stopSelf(); + } + }; + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java new file mode 100644 index 000000000..b64b1482c --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.template.settings; + +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.MenuItem; + +public class SettingsActivity extends ActionBarActivity { + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + // Display the fragment as the main content. + getFragmentManager().beginTransaction().replace(android.R.id.content, new SettingsFragment()).commit(); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java new file mode 100644 index 000000000..947f8d775 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.template.settings; + +import android.os.Bundle; +import android.preference.PreferenceFragment; + +import no.nordicsemi.android.nrftoolbox.R; + +public class SettingsFragment extends PreferenceFragment { + public static final String SETTINGS_DATA = "settings_template_data"; // TODO values matching those in settings_template.xml file in /res/xml + public static final int SETTINGS_VARIANT_A = 0; + public static final int SETTINGS_VARIANT_B = 1; + public static final int SETTINGS_VARIANT_DEFAULT = SETTINGS_VARIANT_A; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.settings_template); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_stat_notify_template.png b/app/src/main/res/drawable-hdpi/ic_stat_notify_template.png new file mode 100644 index 0000000000000000000000000000000000000000..fc09568bc888db13f79c8e5c2cbb433a32f4cfae GIT binary patch literal 546 zcmV+-0^R+IP)4e-6vzK>cgR6dLr9Y}sYQfTA}Ov4L=fu`ORos|0Qmw*v?%m~f<4Nuh1H1F)&HFL?X7-IV(@Zn{2Lk4RxoLq}2j2O% z{x3mVzz5)p-@5XBt(yc{26h;(9bma>m;<2a1LhSl16vO`XaZy#xEKOa1$O~#PYMwM zkAYL_)2W{`8W&~_IH5V60BdEjtpMUAeU@}zQrz(V=u7%4>7ki1=qxVXBT3&RWjC;4 zV@VGrz3~h2Cy<$aG_z-to=AGzAWT=%xqnVVgDd@xW?f0$EX(f3as0E^@6_+#i{p4v z(xX;0GG1RQ6Q_RP$jb4d~6y2ro*&;gP$c2nYg77BB%0^j|ZS78nf zRi5WAP1C3_;qeGC$DvP}rcs{fZek!{1IU2qz*5k^0vwfq904oAxFzqK0ULDywS!<^ zfKG}1wQuc$t>b+LYS#@k&?T@4Sjl+vw#yQ~p+M0!b9~;PBZt6d0JACS(A(xK{7q7; z?C5bLo~VD*uA~>hd;k742vjxK4@oa2-SNMweB3R+RYUo))kIyi9~5Y#s0Zxdo)UH{ ki~Om*22Jfnai&|ZU($6IP{#gyO#lD@07*qoM6N<$f-8voc>n+a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_template_feature.png b/app/src/main/res/drawable-hdpi/ic_template_feature.png new file mode 100644 index 0000000000000000000000000000000000000000..fef8a4174f8ddbc46ebb97403902ff6328861e3c GIT binary patch literal 2597 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 bon8N+S5}8h8E=2#pA^!?>%@a&Vb}f*YB2Om literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_notify_template.png b/app/src/main/res/drawable-xhdpi/ic_stat_notify_template.png new file mode 100644 index 0000000000000000000000000000000000000000..8b4925d3f68b5ec865dd08fea28ae01ec1bdc031 GIT binary patch literal 658 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 a2UP3r%#UnIA@`nA0D0juqT2j=(tiL&K}T`` literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_feature_template.xml b/app/src/main/res/layout/activity_feature_template.xml new file mode 100644 index 000000000..6e282fe26 --- /dev/null +++ b/app/src/main/res/layout/activity_feature_template.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +