LAMP_Connect_v1

This commit is contained in:
Kapil
2019-12-03 15:21:55 +05:30
commit 8bb6805f69
531 changed files with 44089 additions and 0 deletions

2
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build
*.iml

73
app/build.gradle Normal file
View File

@@ -0,0 +1,73 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
defaultConfig {
applicationId "no.nordicsemi.android.nrftoolbox"
minSdkVersion 18
targetSdkVersion 28
versionCode 69
versionName "2.7.2"
resConfigs "en"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
debug {
minifyEnabled true
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation project(':common')
implementation fileTree(dir: 'libs', include: ['*.jar'])
wearApp project(path: ':wear')
// nRF Toolbox is using Play Service 10.2.0 in order to make the app working in China:
// https://developer.android.com/training/wearables/apps/creating-app-china.html
//noinspection GradleDependency
implementation 'com.google.android.gms:play-services-wearable:10.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'
implementation 'androidx.preference:preference:1.1.0-alpha04'
implementation 'com.google.android.material:material:1.1.0-alpha05'
implementation 'no.nordicsemi.android:log:2.2.0'
implementation 'no.nordicsemi.android.support.v18:scanner:1.4.0'
// The DFU Library is imported automatically from jcenter:
implementation 'no.nordicsemi.android:dfu:1.9.0'
// if you desire to build the DFU Library, clone the
// https://github.com/NordicSemiconductor/Android-DFU-Library project into DFULibrary folder,
// add it as a module into the project structure and uncomment the following line
// (and also the according lines in the settings.gradle):
// implementation project(':dfu')
// Import the BLE Common Library.
// The BLE Common Library depends on BLE Library. It is enough to include the first one.
implementation 'no.nordicsemi.android:ble-common:2.1.1'
// The BLE Common Library may be included from jcenter. If you want to modify the code,
// clone both projects from GitHub and replace the line above with the following
// (and also the according lines in the settings.gradle):
// implementation project(':ble-common')
implementation('org.simpleframework:simple-xml:2.7.1') {
exclude group: 'stax', module: 'stax-api'
exclude group: 'xpp3', module: 'xpp3'
}
implementation 'com.android.support:design:28.0.0'
}

Binary file not shown.

81
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,81 @@
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontskipnonpubliclibraryclassmembers
-dontpreverify
-dontshrink
-verbose
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-keepattributes *Annotation*
-keepattributes Signature
-keepclasseswithmembernames class * {
native <methods>;
}
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
# The AndroidX library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older platform version.
-dontwarn androidx.**
-keep class com.google.android.gms.**
-dontwarn com.google.android.gms.**
# Java
-keep class java.** { *; }
-dontnote java.**
-dontwarn java.**
-keep class javax.** { *; }
-dontnote javax.**
-dontwarn javax.**
-keep class sun.misc.Unsafe { *; }
-dontnote sun.misc.Unsafe
-keep class javax.xml.stream.XMLOutputFactory { *; }
# (the rt.jar has them)
-dontwarn com.bea.xml.stream.XMLWriterBase
-dontwarn javax.xml.stream.events.**
-dontwarn javax.xml.stream.**
# Simple XML
-keep public class org.simpleframework.** { *; }
-keep class org.simpleframework.xml.** { *; }
-keep class org.simpleframework.xml.core.** { *; }
-keep class org.simpleframework.xml.util.** { *; }
-keepattributes ElementList, Root, InnerClasses, LineNumberTable
-keepclasseswithmembers class * {
@org.simpleframework.xml.* <fields>;
}
# Chart Engine
-keep class org.achartengine.** { *; }
-dontnote org.achartengine.**
# HTTP (might require legacyLibraries) ?
-dontnote org.apache.http.params.**
-dontnote org.apache.http.conn.scheme.**
-dontnote org.apache.http.conn.**
-dontnote android.net.http.**
# DFU Library
-keep class no.nordicsemi.android.dfu.** { *; }

View File

@@ -0,0 +1,35 @@
/*
* 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;
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);
}
}

View File

@@ -0,0 +1,297 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="no.nordicsemi.android.nrftoolbox"
android:installLocation="auto">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="no.nordicsemi.android.LOG" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<application
android:fullBackupContent="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name=".ToolboxApplication"
tools:ignore="GoogleAppIndexingWarning" >
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<activity
android:name="no.nordicsemi.android.nrftoolbox.SplashscreenActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.SplashScreen" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.no.nordicsemi.type.app"/>
<data android:mimeType="application/vnd.no.nordicsemi.type.address"/>
</intent-filter>
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.FeaturesActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.TranslucentStatusBar"
android:launchMode="singleTask" >
</activity>
<!-- Template plugin activities -->
<!-- Remember to add your plug-in Activities to the Android Manifest file. -->
<activity
android:name="no.nordicsemi.android.nrftoolbox.template.TemplateActivity"
android:icon="@drawable/ic_template_feature"
android:label="@string/template_feature_title" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.template.settings.SettingsActivity"
android:label="@string/template_settings_title" />
<service
android:name="no.nordicsemi.android.nrftoolbox.template.TemplateService"
android:label="@string/template_feature_title" />
<!-- Plug-in activities -->
<activity
android:name="no.nordicsemi.android.nrftoolbox.dfu.DfuActivity"
android:icon="@drawable/ic_dfu_feature"
android:label="@string/dfu_feature_title" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsActivity"
android:label="@string/dfu_settings_title" />
<activity
android:name="no.nordicsemi.android.nrftoolbox.dfu.NotificationActivity"
android:label="@string/dfu_feature_title" />
<activity
android:name="no.nordicsemi.android.nrftoolbox.hrs.HRSActivity"
android:icon="@drawable/ic_hrs_feature"
android:label="@string/hrs_feature_title" >
<!--<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>-->
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.hts.HTSActivity"
android:icon="@drawable/ic_hts_feature"
android:label="@string/hts_feature_title" >
<!-- <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
--> </activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.hts.settings.SettingsActivity"
android:label="@string/hts_settings_title" />
<activity
android:name="no.nordicsemi.android.nrftoolbox.bpm.BPMActivity"
android:icon="@drawable/ic_bpm_feature"
android:label="@string/bpm_feature_title" >
<!-- <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>-->
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.gls.GlucoseActivity"
android:icon="@drawable/ic_glucose_feature"
android:label="@string/gls_feature_title" >
<!--
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
-->
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.proximity.ProximityActivity"
android:icon="@drawable/ic_proximity_feature"
android:label="@string/proximity_feature_title"
android:launchMode="singleTask" >
<!--
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
-->
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.rsc.RSCActivity"
android:icon="@drawable/ic_rsc_feature"
android:label="@string/rsc_feature_title"
android:launchMode="singleTask" >
<!--
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
-->
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.rsc.settings.SettingsActivity"
android:label="@string/rsc_settings_title" />
<activity
android:name="no.nordicsemi.android.nrftoolbox.csc.CSCActivity"
android:icon="@drawable/ic_csc_feature"
android:label="@string/csc_feature_title"
android:launchMode="singleTask" >
<!--
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
-->
</activity>
<activity
android:name="no.nordicsemi.android.nrftoolbox.csc.settings.SettingsActivity"
android:label="@string/csc_settings_title" />
<activity
android:name="no.nordicsemi.android.nrftoolbox.uart.UARTActivity"
android:icon="@drawable/ic_uart_feature"
android:label="@string/uart_feature_title"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask" >
<!--
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
-->
</activity>
<activity android:name="no.nordicsemi.android.nrftoolbox.cgms.CGMSActivity"
android:icon="@drawable/ic_cgms_feature"
android:label="@string/cgms_feature_title"
android:launchMode="singleTask">
<!--
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</intent-filter>
-->
</activity>
<service
android:name="no.nordicsemi.android.nrftoolbox.proximity.ProximityService"
android:label="@string/proximity_feature_title" />
<service
android:name="no.nordicsemi.android.nrftoolbox.dfu.DfuService"
android:exported="true"
android:label="@string/dfu_service_title" >
<!--
<intent-filter>
<action android:name="no.nordicsemi.android.action.DFU_UPLOAD" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
-->
</service>
<activity
android:name="no.nordicsemi.android.nrftoolbox.dfu.DfuInitiatorActivity"
android:label="@string/dfu_service_title"
android:noHistory="true"
android:theme="@style/AppTheme.Translucent" >
<!--
<intent-filter>
<action android:name="no.nordicsemi.android.action.DFU_UPLOAD" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
-->
</activity>
<service
android:name="no.nordicsemi.android.nrftoolbox.cgms.CGMService"
android:label="@string/cgms_feature_title" />
<service
android:name="no.nordicsemi.android.nrftoolbox.rsc.RSCService"
android:label="@string/rsc_feature_title" />
<service
android:name="no.nordicsemi.android.nrftoolbox.csc.CSCService"
android:label="@string/csc_feature_title" />
<service
android:name="no.nordicsemi.android.nrftoolbox.hts.HTSService"
android:label="@string/hts_feature_title" />
<service
android:name="no.nordicsemi.android.nrftoolbox.uart.UARTService"
android:label="@string/uart_feature_title" />
<service android:name=".wearable.MainWearableListenerService">
<!--
<intent-filter>
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/nrftoolbox" />
</intent-filter>
-->
</service>
<provider android:name="no.nordicsemi.android.nrftoolbox.uart.UARTLocalLogContentProvider"
android:authorities="no.nordicsemi.android.nrftoolbox.uart.log"
android:exported="true"/>
</application>
</manifest>

View File

@@ -0,0 +1,75 @@
/*
* 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;
import android.app.Dialog;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
public class AppHelpFragment extends DialogFragment {
private static final String ARG_TEXT = "ARG_TEXT";
private static final String ARG_VERSION = "ARG_VERSION";
public static AppHelpFragment getInstance(final int aboutResId, final boolean appendVersion) {
final AppHelpFragment fragment = new AppHelpFragment();
final Bundle args = new Bundle();
args.putInt(ARG_TEXT, aboutResId);
args.putBoolean(ARG_VERSION, appendVersion);
fragment.setArguments(args);
return fragment;
}
public static AppHelpFragment getInstance(final int aboutResId) {
final AppHelpFragment fragment = new AppHelpFragment();
final Bundle args = new Bundle();
args.putInt(ARG_TEXT, aboutResId);
args.putBoolean(ARG_VERSION, false);
fragment.setArguments(args);
return fragment;
}
@Override
@NonNull
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Bundle args = getArguments();
final StringBuilder text = new StringBuilder(getString(args.getInt(ARG_TEXT)));
final boolean appendVersion = args.getBoolean(ARG_VERSION);
if (appendVersion) {
try {
final String version = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionName;
text.append(getString(R.string.about_version, version));
} catch (final NameNotFoundException e) {
// do nothing
}
}
return new AlertDialog.Builder(getActivity()).setTitle(R.string.about_title).setMessage(text)
.setPositiveButton(R.string.ok, null).create();
}
}

View File

@@ -0,0 +1,224 @@
/*
* 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;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.List;
import no.nordicsemi.android.nrftoolbox.adapter.AppAdapter;
import no.nordicsemi.android.nrftoolbox.hrs.HRSActivity;
public class FeaturesActivity extends AppCompatActivity {
private static final String NRF_CONNECT_CATEGORY = "no.nordicsemi.android.nrftoolbox.LAUNCHER";
private static final String UTILS_CATEGORY = "no.nordicsemi.android.nrftoolbox.UTILS";
private static final String NRF_CONNECT_PACKAGE = "no.nordicsemi.android.mcp";
private static final String NRF_CONNECT_CLASS = NRF_CONNECT_PACKAGE + ".DeviceListActivity";
private static final String NRF_CONNECT_MARKET_URI = "market://details?id=no.nordicsemi.android.mcp";
// Extras that can be passed from NFC (see SplashscreenActivity)
public static final String EXTRA_APP = "application/vnd.no.nordicsemi.type.app";
public static final String EXTRA_ADDRESS = "application/vnd.no.nordicsemi.type.address";
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_features);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
// ensure that Bluetooth exists
if (!ensureBLEExists())
finish();
final DrawerLayout drawer = mDrawerLayout = findViewById(R.id.drawer_layout);
drawer.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
// Set the drawer toggle as the DrawerListener
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.drawer_open, R.string.drawer_close) {
@Override
public void onDrawerSlide(final View drawerView, final float slideOffset) {
// Disable the Hamburger icon animation
super.onDrawerSlide(drawerView, 0);
}
};
drawer.addDrawerListener(mDrawerToggle);
// setup plug-ins in the drawer
//setupPluginsInDrawer(drawer.findViewById(R.id.plugin_container));
// configure the app grid
final GridView grid = findViewById(R.id.grid);
grid.setAdapter(new AppAdapter(this));
grid.setEmptyView(findViewById(android.R.id.empty));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
final Intent intent = getIntent();
if (intent.hasExtra(EXTRA_APP) && intent.hasExtra(EXTRA_ADDRESS)) {
final String app = intent.getStringExtra(EXTRA_APP);
switch (app) {
case "HRM":
final Intent newIntent = new Intent(this, HRSActivity.class);
newIntent.putExtra(EXTRA_ADDRESS, intent.getByteArrayExtra(EXTRA_ADDRESS));
newIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(newIntent);
break;
default:
// other are not supported yet
break;
}
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.help, menu);
return true;
}
@Override
protected void onPostCreate(final Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
mDrawerToggle.syncState();
}
@Override
public void onConfigurationChanged(@NonNull final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Pass the event to ActionBarDrawerToggle, if it returns
// true, then it has handled the app icon touch event
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
switch (item.getItemId()) {
case R.id.action_about:
final AppHelpFragment fragment = AppHelpFragment.getInstance(R.string.about_text, true);
fragment.show(getSupportFragmentManager(), null);
break;
}
return true;
}
/*
private void setupPluginsInDrawer(final ViewGroup container) {
final LayoutInflater inflater = LayoutInflater.from(this);
final PackageManager pm = getPackageManager();
// look for nRF Connect
final Intent nrfConnectIntent = new Intent(Intent.ACTION_MAIN);
nrfConnectIntent.addCategory(NRF_CONNECT_CATEGORY);
nrfConnectIntent.setClassName(NRF_CONNECT_PACKAGE, NRF_CONNECT_CLASS);
final ResolveInfo nrfConnectInfo = pm.resolveActivity(nrfConnectIntent, 0);
// configure link to nRF Connect
final TextView nrfConnectItem = container.findViewById(R.id.link_mcp);
if (nrfConnectInfo == null) {
nrfConnectItem.setTextColor(Color.GRAY);
final ColorMatrix grayscale = new ColorMatrix();
grayscale.setSaturation(0.0f);
nrfConnectItem.getCompoundDrawables()[0].mutate().setColorFilter(new ColorMatrixColorFilter(grayscale));
}
nrfConnectItem.setOnClickListener(v -> {
Intent action = nrfConnectIntent;
if (nrfConnectInfo == null)
action = new Intent(Intent.ACTION_VIEW, Uri.parse(NRF_CONNECT_MARKET_URI));
action.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
action.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(action);
} catch (final ActivityNotFoundException e) {
Toast.makeText(FeaturesActivity.this, R.string.no_application_play, Toast.LENGTH_SHORT).show();
}
mDrawerLayout.closeDrawers();
});
// look for other plug-ins
final Intent utilsIntent = new Intent(Intent.ACTION_MAIN);
utilsIntent.addCategory(UTILS_CATEGORY);
final List<ResolveInfo> appList = pm.queryIntentActivities(utilsIntent, 0);
for (final ResolveInfo info : appList) {
final View item = inflater.inflate(R.layout.drawer_plugin, container, false);
final ImageView icon = item.findViewById(android.R.id.icon);
final TextView label = item.findViewById(android.R.id.text1);
label.setText(info.loadLabel(pm));
icon.setImageDrawable(info.loadIcon(pm));
item.setOnClickListener(v -> {
final Intent intent = new Intent();
intent.setComponent(new ComponentName(info.activityInfo.packageName, info.activityInfo.name));
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
mDrawerLayout.closeDrawers();
});
container.addView(item);
}
}
*/
private boolean ensureBLEExists() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show();
return false;
}
return true;
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
public class PermissionRationaleFragment extends DialogFragment {
private static final String ARG_PERMISSION = "ARG_PERMISSION";
private static final String ARG_TEXT = "ARG_TEXT";
private PermissionDialogListener mListener;
public interface PermissionDialogListener {
void onRequestPermission(final String permission);
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
if (context instanceof PermissionDialogListener) {
mListener = (PermissionDialogListener) context;
} else {
throw new IllegalArgumentException("The parent activity must impelemnt PermissionDialogListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
public static PermissionRationaleFragment getInstance(final int aboutResId, final String permission) {
final PermissionRationaleFragment fragment = new PermissionRationaleFragment();
final Bundle args = new Bundle();
args.putInt(ARG_TEXT, aboutResId);
args.putString(ARG_PERMISSION, permission);
fragment.setArguments(args);
return fragment;
}
@Override
@NonNull
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Bundle args = getArguments();
final StringBuilder text = new StringBuilder(getString(args.getInt(ARG_TEXT)));
return new AlertDialog.Builder(getActivity()).setTitle(R.string.permission_title).setMessage(text)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialog, which) -> mListener.onRequestPermission(args.getString(ARG_PERMISSION))).create();
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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;
import android.app.Activity;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
public class SplashscreenActivity extends Activity {
/** Splash screen duration time in milliseconds */
private static final int DELAY = 1000;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splashscreen);
// Jump to SensorsActivity after DELAY milliseconds
new Handler().postDelayed(() -> {
final Intent newIntent = new Intent(SplashscreenActivity.this, FeaturesActivity.class);
newIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
// Handle NFC message, if app was opened using NFC AAR record
final Intent intent = getIntent();
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
final Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMsgs != null) {
for (Parcelable rawMsg : rawMsgs) {
final NdefMessage msg = (NdefMessage) rawMsg;
final NdefRecord[] records = msg.getRecords();
for (NdefRecord record : records) {
if (record.getTnf() == NdefRecord.TNF_MIME_MEDIA) {
switch (record.toMimeType()) {
case FeaturesActivity.EXTRA_APP:
newIntent.putExtra(FeaturesActivity.EXTRA_APP, new String(record.getPayload()));
break;
case FeaturesActivity.EXTRA_ADDRESS:
newIntent.putExtra(FeaturesActivity.EXTRA_ADDRESS, invertEndianness(record.getPayload()));
break;
}
}
}
}
}
}
startActivity(newIntent);
finish();
}, DELAY);
}
@Override
public void onBackPressed() {
// do nothing. Protect from exiting the application when splash screen is shown
}
/**
* Inverts endianness of the byte array.
* @param bytes input byte array
* @return byte array in opposite order
*/
private byte[] invertEndianness(final byte[] bytes) {
if (bytes == null)
return null;
final int length = bytes.length;
final byte[] result = new byte[length];
for (int i = 0; i < length; i++)
result[i] = bytes[length - i - 1];
return result;
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2017, 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;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import no.nordicsemi.android.dfu.DfuServiceInitiator;
public class ToolboxApplication extends Application {
public static final String CONNECTED_DEVICE_CHANNEL = "connected_device_channel";
public static final String FILE_SAVED_CHANNEL = "file_saved_channel";
public static final String PROXIMITY_WARNINGS_CHANNEL = "proximity_warnings_channel";
@Override
public void onCreate() {
super.onCreate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
DfuServiceInitiator.createDfuNotificationChannel(this);
final NotificationChannel channel = new NotificationChannel(CONNECTED_DEVICE_CHANNEL, getString(R.string.channel_connected_devices_title), NotificationManager.IMPORTANCE_LOW);
channel.setDescription(getString(R.string.channel_connected_devices_description));
channel.setShowBadge(false);
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
final NotificationChannel fileChannel = new NotificationChannel(FILE_SAVED_CHANNEL, getString(R.string.channel_files_title), NotificationManager.IMPORTANCE_LOW);
fileChannel.setDescription(getString(R.string.channel_files_description));
fileChannel.setShowBadge(false);
fileChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
final NotificationChannel proximityChannel = new NotificationChannel(PROXIMITY_WARNINGS_CHANNEL, getString(R.string.channel_proximity_warnings_title), NotificationManager.IMPORTANCE_LOW);
proximityChannel.setDescription(getString(R.string.channel_proximity_warnings_description));
proximityChannel.setShowBadge(false);
proximityChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
notificationManager.createNotificationChannel(fileChannel);
notificationManager.createNotificationChannel(proximityChannel);
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.adapter;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import no.nordicsemi.android.nrftoolbox.R;
public class AppAdapter extends BaseAdapter {
private static final String CATEGORY = "no.nordicsemi.android.nrftoolbox.LAUNCHER";
private static final String NRF_CONNECT_PACKAGE = "no.nordicsemi.android.mcp";
private final Context mContext;
private final PackageManager mPackageManager;
private final LayoutInflater mInflater;
private final List<ResolveInfo> mApplications;
public AppAdapter(final Context context) {
mContext = context;
mInflater = LayoutInflater.from(context);
// get nRF installed app plugins from package manager
final PackageManager pm = mPackageManager = context.getPackageManager();
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(CATEGORY);
final List<ResolveInfo> appList = mApplications = pm.queryIntentActivities(intent, 0);
// TODO remove the following loop after some time, when there will be no more MCP 1.1 at the market.
for (final ResolveInfo info : appList) {
if (NRF_CONNECT_PACKAGE.equals(info.activityInfo.packageName)) {
appList.remove(info);
break;
}
}
Collections.sort(appList, new ResolveInfo.DisplayNameComparator(pm));
}
@Override
public int getCount() {
return mApplications.size();
}
@Override
public Object getItem(int position) {
return mApplications.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.feature_icon, parent, false);
final ViewHolder holder = new ViewHolder();
holder.view = view;
holder.icon = view.findViewById(R.id.icon);
holder.label = view.findViewById(R.id.label);
view.setTag(holder);
}
final ResolveInfo info = mApplications.get(position);
final PackageManager pm = mPackageManager;
final ViewHolder holder = (ViewHolder) view.getTag();
holder.icon.setImageDrawable(info.loadIcon(pm));
holder.label.setText(info.loadLabel(pm).toString().toUpperCase(Locale.US));
holder.view.setOnClickListener(v -> {
final Intent intent = new Intent();
intent.setComponent(new ComponentName(info.activityInfo.packageName, info.activityInfo.name));
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
mContext.startActivity(intent);
});
return view;
}
private class ViewHolder {
private View view;
private ImageView icon;
private TextView label;
}
}

View File

@@ -0,0 +1,299 @@
/*
* Copyright (C) 2006 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.
*/
package no.nordicsemi.android.nrftoolbox.app;
import android.app.Activity;
import android.database.Cursor;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.SimpleCursorTreeAdapter;
import android.widget.SimpleExpandableListAdapter;
import java.util.List;
import java.util.Map;
import no.nordicsemi.android.nrftoolbox.R;
/**
* An activity that displays an expandable list of items by binding to a data source implementing the ExpandableListAdapter, and exposes event handlers when the
* user selects an item.
* <p>
* ExpandableListActivity hosts a {@link android.widget.ExpandableListView ExpandableListView} object that can be bound to different data sources that provide a
* two-levels of data (the top-level is group, and below each group are children). Binding, screen layout, and row layout are discussed in the following
* sections.
* <p>
* <strong>Screen Layout</strong>
* </p>
* <p>
* ExpandableListActivity has a default layout that consists of a single, full-screen, centered expandable list. However, if you desire, you can customize the
* screen layout by setting your own view layout with setContentView() in onCreate(). To do this, your own view MUST contain an ExpandableListView object with
* the id "@android:id/list" (or {@link android.R.id#list} if it's in code)
* <p>
* Optionally, your custom view can contain another view object of any type to display when the list view is empty. This "empty list" notifier must have an id
* "android:empty". Note that when an empty view is present, the expandable list view will be hidden when there is no data to display.
* <p>
* The following code demonstrates an (ugly) custom screen layout. It has a list with a green background, and an alternate red "no data" message.
* </p>
*
* <pre>
* &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
* &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
* android:orientation=&quot;vertical&quot;
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;match_parent&quot;
* android:paddingLeft=&quot;8dp&quot;
* android:paddingRight=&quot;8dp&quot;&gt;
*
* &lt;ExpandableListView android:id=&quot;@id/android:list&quot;
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;match_parent&quot;
* android:background=&quot;#00FF00&quot;
* android:layout_weight=&quot;1&quot;
* android:drawSelectorOnTop=&quot;false&quot;/&gt;
*
* &lt;TextView android:id=&quot;@id/android:empty&quot;
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;match_parent&quot;
* android:background=&quot;#FF0000&quot;
* android:text=&quot;No data&quot;/&gt;
* &lt;/LinearLayout&gt;
* </pre>
*
* <p>
* <strong>Row Layout</strong>
* </p>
* The {@link ExpandableListAdapter} set in the {@link ExpandableListActivity} via {@link #setListAdapter(ExpandableListAdapter)} provides the {@link View}s for
* each row. This adapter has separate methods for providing the group {@link View}s and child {@link View}s. There are a couple provided
* {@link ExpandableListAdapter}s that simplify use of adapters: {@link SimpleCursorTreeAdapter} and {@link SimpleExpandableListAdapter}.
* <p>
* With these, you can specify the layout of individual rows for groups and children in the list. These constructor takes a few parameters that specify layout
* resources for groups and children. It also has additional parameters that let you specify which data field to associate with which object in the row layout
* resource. The {@link SimpleCursorTreeAdapter} fetches data from {@link Cursor}s and the {@link SimpleExpandableListAdapter} fetches data from {@link List}s
* of {@link Map}s.
* </p>
* <p>
* Android provides some standard row layout resources. These are in the {@link android.R.layout} class, and have names such as simple_list_item_1,
* simple_list_item_2, and two_line_list_item. The following layout XML is the source for the resource two_line_list_item, which displays two data fields,one
* above the other, for each list row.
* </p>
*
* <pre>
* &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
* &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;wrap_content&quot;
* android:orientation=&quot;vertical&quot;&gt;
*
* &lt;TextView android:id=&quot;@+id/text1&quot;
* android:textSize=&quot;16sp&quot;
* android:textStyle=&quot;bold&quot;
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;wrap_content&quot;/&gt;
*
* &lt;TextView android:id=&quot;@+id/text2&quot;
* android:textSize=&quot;16sp&quot;
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;wrap_content&quot;/&gt;
* &lt;/LinearLayout&gt;
* </pre>
*
* <p>
* You must identify the data bound to each TextView object in this layout. The syntax for this is discussed in the next section.
* </p>
* <p>
* <strong>Binding to Data</strong>
* </p>
* <p>
* You bind the ExpandableListActivity's ExpandableListView object to data using a class that implements the {@link android.widget.ExpandableListAdapter
* ExpandableListAdapter} interface. Android provides two standard list adapters: {@link android.widget.SimpleExpandableListAdapter SimpleExpandableListAdapter}
* for static data (Maps), and {@link android.widget.SimpleCursorTreeAdapter SimpleCursorTreeAdapter} for Cursor query results.
* </p>
*
* @see #setListAdapter
* @see android.widget.ExpandableListView
*/
public class ExpandableListActivity extends AppCompatActivity implements
OnCreateContextMenuListener,
ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener,
ExpandableListView.OnGroupExpandListener {
ExpandableListAdapter mAdapter;
ExpandableListView mList;
boolean mFinishedStart = false;
/**
* Override this to populate the context menu when an item is long pressed. menuInfo will contain an
* {@link android.widget.ExpandableListView.ExpandableListContextMenuInfo} whose packedPosition is a packed position that should be used with
* {@link ExpandableListView#getPackedPositionType(long)} and the other similar methods.
* <p>
* {@inheritDoc}
*/
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
}
/**
* Override this for receiving callbacks when a child has been clicked.
* <p>
* {@inheritDoc}
*/
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
int childPosition, long id) {
return false;
}
/**
* Override this for receiving callbacks when a group has been collapsed.
*/
@Override
public void onGroupCollapse(int groupPosition) {
}
/**
* Override this for receiving callbacks when a group has been expanded.
*/
@Override
public void onGroupExpand(int groupPosition) {
}
/**
* Ensures the expandable list view has been created before Activity restores all of the view states.
*
* @see Activity#onRestoreInstanceState(Bundle)
*/
@Override
protected void onRestoreInstanceState(@NonNull Bundle state) {
ensureList();
super.onRestoreInstanceState(state);
}
/**
* Updates the screen state (current list and other views) when the content changes.
*
* @see androidx.appcompat.app.AppCompatActivity#onContentChanged()
*/
@Override
public void onContentChanged() {
super.onContentChanged();
View emptyView = findViewById(R.id.empty);
mList = findViewById(R.id.list);
if (mList == null) {
throw new RuntimeException(
"Your content must have a ExpandableListView whose id attribute is " +
"'R.id.list'");
}
if (emptyView != null) {
mList.setEmptyView(emptyView);
}
mList.setOnChildClickListener(this);
mList.setOnGroupExpandListener(this);
mList.setOnGroupCollapseListener(this);
if (mFinishedStart) {
setListAdapter(mAdapter);
}
mFinishedStart = true;
}
/**
* Provide the adapter for the expandable list.
*/
public void setListAdapter(ExpandableListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
/**
* Get the activity's expandable list view widget. This can be used to get the selection, set the selection, and many other useful functions.
*
* @see ExpandableListView
*/
public ExpandableListView getExpandableListView() {
ensureList();
return mList;
}
/**
* Get the ExpandableListAdapter associated with this activity's ExpandableListView.
*/
public ExpandableListAdapter getExpandableListAdapter() {
return mAdapter;
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(R.layout.expandable_list_content);
}
/**
* Gets the ID of the currently selected group or child.
*
* @return The ID of the currently selected group or child.
*/
public long getSelectedId() {
return mList.getSelectedId();
}
/**
* Gets the position (in packed position representation) of the currently selected group or child. Use {@link ExpandableListView#getPackedPositionType},
* {@link ExpandableListView#getPackedPositionGroup}, and {@link ExpandableListView#getPackedPositionChild} to unpack the returned packed position.
*
* @return A packed position representation containing the currently selected group or child's position and type.
*/
public long getSelectedPosition() {
return mList.getSelectedPosition();
}
/**
* Sets the selection to the specified child. If the child is in a collapsed group, the group will only be expanded and child subsequently selected if
* shouldExpandGroup is set to true, otherwise the method will return false.
*
* @param groupPosition
* The position of the group that contains the child.
* @param childPosition
* The position of the child within the group.
* @param shouldExpandGroup
* Whether the child's group should be expanded if it is collapsed.
* @return Whether the selection was successfully set on the child.
*/
public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
return mList.setSelectedChild(groupPosition, childPosition, shouldExpandGroup);
}
/**
* Sets the selection to the specified group.
*
* @param groupPosition
* The position of the group that should be selected.
*/
public void setSelectedGroup(int groupPosition) {
mList.setSelectedGroup(groupPosition);
}
}

View File

@@ -0,0 +1,124 @@
package no.nordicsemi.android.nrftoolbox.battery;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.callback.DataReceivedCallback;
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
/**
* The Ble Manager with Battery Service support.
*
* @param <T> The profile callbacks type.
* @see BleManager
*/
@SuppressWarnings("WeakerAccess")
public abstract class BatteryManager<T extends BatteryManagerCallbacks> extends LoggableBleManager<T> {
/** Battery Service UUID. */
private final static UUID BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb");
/** Battery Level characteristic UUID. */
private final static UUID BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mBatteryLevelCharacteristic;
/** Last received Battery Level value. */
private Integer mBatteryLevel;
/**
* The manager constructor.
*
* @param context context.
*/
public BatteryManager(final Context context) {
super(context);
}
private DataReceivedCallback mBatteryLevelDataCallback = new BatteryLevelDataCallback() {
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
log(LogContract.Log.Level.APPLICATION,"Battery Level received: " + batteryLevel + "%");
mBatteryLevel = batteryLevel;
mCallbacks.onBatteryLevelChanged(device, batteryLevel);
}
@Override
public void onInvalidDataReceived(@NonNull final BluetoothDevice device, final @NonNull Data data) {
log(Log.WARN, "Invalid Battery Level data received: " + data);
}
};
public void readBatteryLevelCharacteristic() {
if (isConnected()) {
readCharacteristic(mBatteryLevelCharacteristic)
.with(mBatteryLevelDataCallback)
.fail((device, status) -> log(Log.WARN,"Battery Level characteristic not found"))
.enqueue();
}
}
public void enableBatteryLevelCharacteristicNotifications() {
if (isConnected()) {
// If the Battery Level characteristic is null, the request will be ignored
setNotificationCallback(mBatteryLevelCharacteristic)
.with(mBatteryLevelDataCallback);
enableNotifications(mBatteryLevelCharacteristic)
.done(device -> log(Log.INFO, "Battery Level notifications enabled"))
.fail((device, status) -> log(Log.WARN, "Battery Level characteristic not found"))
.enqueue();
}
}
/**
* Disables Battery Level notifications on the Server.
*/
public void disableBatteryLevelCharacteristicNotifications() {
if (isConnected()) {
disableNotifications(mBatteryLevelCharacteristic)
.done(device -> log(Log.INFO, "Battery Level notifications disabled"))
.enqueue();
}
}
/**
* Returns the last received Battery Level value.
* The value is set to null when the device disconnects.
* @return Battery Level value, in percent.
*/
public Integer getBatteryLevel() {
return mBatteryLevel;
}
protected abstract class BatteryManagerGattCallback extends BleManagerGattCallback {
@Override
protected void initialize() {
readBatteryLevelCharacteristic();
enableBatteryLevelCharacteristicNotifications();
}
@Override
protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(BATTERY_SERVICE_UUID);
if (service != null) {
mBatteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID);
}
return mBatteryLevelCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
mBatteryLevelCharacteristic = null;
mBatteryLevel = null;
}
}
}

View File

@@ -0,0 +1,7 @@
package no.nordicsemi.android.nrftoolbox.battery;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback;
public interface BatteryManagerCallbacks extends BleManagerCallbacks, BatteryLevelCallback {
}

View File

@@ -0,0 +1,179 @@
/*
* 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.bpm;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.widget.TextView;
import java.util.Calendar;
import java.util.UUID;
import no.nordicsemi.android.ble.common.profile.bp.BloodPressureMeasurementCallback;
import no.nordicsemi.android.ble.common.profile.bp.IntermediateCuffPressureCallback;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileActivity;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
// TODO The BPMActivity should be rewritten to use the service approach, like other do.
public class BPMActivity extends BleProfileActivity implements BPMManagerCallbacks {
@SuppressWarnings("unused")
private static final String TAG = "BPMActivity";
private TextView mSystolicView;
private TextView mSystolicUnitView;
private TextView mDiastolicView;
private TextView mDiastolicUnitView;
private TextView mMeanAPView;
private TextView mMeanAPUnitView;
private TextView mPulseView;
private TextView mTimestampView;
private TextView mBatteryLevelView;
@Override
protected void onCreateView(final Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_bpm);
setGUI();
}
private void setGUI() {
mSystolicView = findViewById(R.id.systolic);
mSystolicUnitView = findViewById(R.id.systolic_unit);
mDiastolicView = findViewById(R.id.diastolic);
mDiastolicUnitView = findViewById(R.id.diastolic_unit);
mMeanAPView = findViewById(R.id.mean_ap);
mMeanAPUnitView = findViewById(R.id.mean_ap_unit);
mPulseView = findViewById(R.id.pulse);
mTimestampView = findViewById(R.id.timestamp);
mBatteryLevelView = findViewById(R.id.battery);
}
@Override
protected int getLoggerProfileTitle() {
return R.string.bpm_feature_title;
}
@Override
protected int getDefaultDeviceName() {
return R.string.bpm_default_name;
}
@Override
protected int getAboutTextId() {
return R.string.bpm_about_text;
}
@Override
protected UUID getFilterUUID() {
return BPMManager.BP_SERVICE_UUID;
}
@Override
protected LoggableBleManager<BPMManagerCallbacks> initializeManager() {
final BPMManager manager = BPMManager.getBPMManager(getApplicationContext());
manager.setGattCallbacks(this);
return manager;
}
@Override
protected void setDefaultUI() {
mSystolicView.setText(R.string.not_available_value);
mSystolicUnitView.setText(null);
mDiastolicView.setText(R.string.not_available_value);
mDiastolicUnitView.setText(null);
mMeanAPView.setText(R.string.not_available_value);
mMeanAPUnitView.setText(null);
mPulseView.setText(R.string.not_available_value);
mTimestampView.setText(R.string.not_available);
mBatteryLevelView.setText(R.string.not_available);
}
@Override
public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) {
// this may notify user or show some views
}
@Override
public void onDeviceReady(@NonNull final BluetoothDevice device) {
// this may notify user
}
@Override
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
super.onDeviceDisconnected(device);
runOnUiThread(() -> mBatteryLevelView.setText(R.string.not_available));
}
@Override
public void onBloodPressureMeasurementReceived(@NonNull final BluetoothDevice device,
final float systolic, final float diastolic, final float meanArterialPressure, final int unit,
@Nullable final Float pulseRate, @Nullable final Integer userID,
@Nullable final BPMStatus status, @Nullable final Calendar calendar) {
runOnUiThread(() -> {
mSystolicView.setText(String.valueOf(systolic));
mDiastolicView.setText(String.valueOf(diastolic));
mMeanAPView.setText(String.valueOf(meanArterialPressure));
if (pulseRate != null)
mPulseView.setText(String.valueOf(pulseRate));
else
mPulseView.setText(R.string.not_available_value);
if (calendar != null)
mTimestampView.setText(getString(R.string.bpm_timestamp, calendar));
else
mTimestampView.setText(R.string.not_available);
mSystolicUnitView.setText(unit == BloodPressureMeasurementCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa);
mDiastolicUnitView.setText(unit == BloodPressureMeasurementCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa);
mMeanAPUnitView.setText(unit == BloodPressureMeasurementCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa);
});
}
@Override
public void onIntermediateCuffPressureReceived(@NonNull final BluetoothDevice device, final float cuffPressure, final int unit,
@Nullable final Float pulseRate, @Nullable final Integer userID,
@Nullable final BPMStatus status, @Nullable final Calendar calendar) {
runOnUiThread(() -> {
mSystolicView.setText(String.valueOf(cuffPressure));
mDiastolicView.setText(R.string.not_available_value);
mMeanAPView.setText(R.string.not_available_value);
if (pulseRate != null)
mPulseView.setText(String.valueOf(pulseRate));
else
mPulseView.setText(R.string.not_available_value);
if (calendar != null)
mTimestampView.setText(getString(R.string.bpm_timestamp, calendar));
else
mTimestampView.setText(R.string.not_available);
mSystolicUnitView.setText(unit == IntermediateCuffPressureCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa);
mDiastolicUnitView.setText(null);
mMeanAPUnitView.setText(null);
});
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
runOnUiThread(() -> mBatteryLevelView.setText(getString(R.string.battery, batteryLevel)));
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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.bpm;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import java.util.Calendar;
import java.util.UUID;
import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementDataCallback;
import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureDataCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.BloodPressureMeasurementParser;
import no.nordicsemi.android.nrftoolbox.parser.IntermediateCuffPressureParser;
@SuppressWarnings({"unused", "WeakerAccess"})
public class BPMManager extends BatteryManager<BPMManagerCallbacks> {
/** Blood Pressure service UUID. */
public final static UUID BP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb");
/** Blood Pressure Measurement characteristic UUID. */
private static final UUID BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb");
/** Intermediate Cuff Pressure characteristic UUID. */
private static final UUID ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mBPMCharacteristic, mICPCharacteristic;
private static BPMManager managerInstance = null;
/**
* Returns the singleton implementation of BPMManager.
*/
public static synchronized BPMManager getBPMManager(final Context context) {
if (managerInstance == null) {
managerInstance = new BPMManager(context);
}
return managerInstance;
}
private BPMManager(final Context context) {
super(context);
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving notification, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
super.initialize();
setNotificationCallback(mICPCharacteristic)
.with(new IntermediateCuffPressureDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + IntermediateCuffPressureParser.parse(data) + "\" received");
// Pass through received data
super.onDataReceived(device, data);
}
@Override
public void onIntermediateCuffPressureReceived(@NonNull final BluetoothDevice device,
final float cuffPressure, final int unit,
@Nullable final Float pulseRate, @Nullable final Integer userID,
@Nullable final BPMStatus status, @Nullable final Calendar calendar) {
mCallbacks.onIntermediateCuffPressureReceived(device, cuffPressure, unit, pulseRate, userID, status, calendar);
}
@Override
public void onInvalidDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(Log.WARN, "Invalid ICP data received: " + data);
}
});
setIndicationCallback(mBPMCharacteristic)
.with(new BloodPressureMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + BloodPressureMeasurementParser.parse(data) + "\" received");
// Pass through received data
super.onDataReceived(device, data);
}
@Override
public void onBloodPressureMeasurementReceived(@NonNull final BluetoothDevice device,
final float systolic, final float diastolic, final float meanArterialPressure,
final int unit, @Nullable final Float pulseRate,
@Nullable final Integer userID, @Nullable final BPMStatus status,
@Nullable final Calendar calendar) {
mCallbacks.onBloodPressureMeasurementReceived(device, systolic, diastolic,
meanArterialPressure, unit, pulseRate, userID, status, calendar);
}
@Override
public void onInvalidDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(Log.WARN, "Invalid BPM data received: " + data);
}
});
enableNotifications(mICPCharacteristic)
.fail((device, status) -> log(Log.WARN,
"Intermediate Cuff Pressure characteristic not found"))
.enqueue();
enableIndications(mBPMCharacteristic).enqueue();
}
@Override
protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(BP_SERVICE_UUID);
if (service != null) {
mBPMCharacteristic = service.getCharacteristic(BPM_CHARACTERISTIC_UUID);
mICPCharacteristic = service.getCharacteristic(ICP_CHARACTERISTIC_UUID);
}
return mBPMCharacteristic != null;
}
@Override
protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
super.isOptionalServiceSupported(gatt); // ignore the result of this
return mICPCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
mICPCharacteristic = null;
mBPMCharacteristic = null;
}
};
}

View File

@@ -0,0 +1,31 @@
/*
* 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.bpm;
import no.nordicsemi.android.ble.common.profile.bp.BloodPressureMeasurementCallback;
import no.nordicsemi.android.ble.common.profile.bp.IntermediateCuffPressureCallback;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
interface BPMManagerCallbacks extends BatteryManagerCallbacks,
BloodPressureMeasurementCallback, IntermediateCuffPressureCallback {
}

View File

@@ -0,0 +1,280 @@
/*
* Copyright (c) 2016, 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.cgms;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.util.SparseArray;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import android.widget.PopupMenu;
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.proximity.ProximityService;
public class CGMSActivity extends BleProfileServiceReadyActivity<CGMService.CGMSBinder> implements PopupMenu.OnMenuItemClickListener {
private View mControlPanelStd;
private View mControlPanelAbort;
private ListView mRecordsListView;
private TextView mBatteryLevelView;
private CGMSRecordsAdapter mCgmsRecordsAdapter;
private CGMService.CGMSBinder mBinder;
@Override
protected void onCreateView(Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_cgms);
setGUI();
}
@Override
protected void onInitialize(Bundle savedInstanceState) {
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, makeIntentFilter());
}
private void setGUI() {
mRecordsListView = findViewById(R.id.list);
mControlPanelStd = findViewById(R.id.cgms_control_std);
mControlPanelAbort = findViewById(R.id.cgms_control_abort);
mBatteryLevelView = findViewById(R.id.battery);
findViewById(R.id.action_last).setOnClickListener(v -> {
clearRecords();
if (mBinder != null) {
mBinder.clear();
mBinder.getLastRecord();
}
});
findViewById(R.id.action_all).setOnClickListener(v -> {
clearRecords();
if (mBinder != null) {
clearRecords();
mBinder.getAllRecords();
}
});
findViewById(R.id.action_abort).setOnClickListener(v -> {
if (mBinder != null) {
mBinder.abort();
}
});
// create popup menu attached to the button More
findViewById(R.id.action_more).setOnClickListener(v -> {
PopupMenu menu = new PopupMenu(CGMSActivity.this, v);
menu.setOnMenuItemClickListener(CGMSActivity.this);
MenuInflater inflater = menu.getMenuInflater();
inflater.inflate(R.menu.gls_more, menu.getMenu());
menu.show();
});
}
private void loadAdapter(SparseArray<CGMSRecord> records) {
mCgmsRecordsAdapter.clear();
for (int i = 0; i < records.size(); i++) {
mCgmsRecordsAdapter.addItem(records.valueAt(i));
}
mCgmsRecordsAdapter.notifyDataSetChanged();
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
}
@Override
protected void onServiceBound(final CGMService.CGMSBinder binder) {
mBinder = binder;
final SparseArray<CGMSRecord> cgmsRecords = binder.getRecords();
if (cgmsRecords != null && cgmsRecords.size() > 0) {
if (mCgmsRecordsAdapter == null) {
mCgmsRecordsAdapter = new CGMSRecordsAdapter(CGMSActivity.this);
mRecordsListView.setAdapter(mCgmsRecordsAdapter);
}
loadAdapter(cgmsRecords);
}
}
@Override
protected void onServiceUnbound() {
mBinder = null;
}
@Override
protected Class<? extends BleProfileService> getServiceClass() {
return CGMService.class;
}
@Override
protected int getLoggerProfileTitle() {
return R.string.cgms_feature_title;
}
@Override
protected int getAboutTextId() {
return R.string.cgms_about_text;
}
@Override
protected int getDefaultDeviceName() {
return R.string.cgms_default_name;
}
@Override
protected UUID getFilterUUID() {
return CGMSManager.CGMS_UUID;
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
// this may notify user or show some views
}
private void setOperationInProgress(final boolean progress) {
runOnUiThread(() -> {
// setSupportProgressBarIndeterminateVisibility(progress);
mControlPanelStd.setVisibility(!progress ? View.VISIBLE : View.GONE);
mControlPanelAbort.setVisibility(progress ? View.VISIBLE : View.GONE);
});
}
public void onBatteryLevelChanged(final BluetoothDevice device, final int value) {
mBatteryLevelView.setText(getString(R.string.battery, value));
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
super.onDeviceDisconnected(device);
setOperationInProgress(false);
mBatteryLevelView.setText(R.string.not_available);
}
@Override
public void onError(final BluetoothDevice device, final String message, final int errorCode) {
super.onError(device, message, errorCode);
setOperationInProgress(false);
}
@Override
protected void setDefaultUI() {
clearRecords();
mBatteryLevelView.setText(R.string.not_available);
}
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.action_refresh:
if(mBinder != null)
mBinder.refreshRecords();
break;
case R.id.action_first:
if (mBinder != null)
mBinder.getFirstRecord();
break;
case R.id.action_clear:
if (mBinder != null)
mBinder.clear();
break;
case R.id.action_delete_all:
if (mBinder != null)
mBinder.deleteAllRecords();
break;
}
return true;
}
private void clearRecords() {
if (mCgmsRecordsAdapter != null) {
mCgmsRecordsAdapter.clear();
mCgmsRecordsAdapter.notifyDataSetChanged();
}
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
final BluetoothDevice device = intent.getParcelableExtra(ProximityService.EXTRA_DEVICE);
switch (action) {
case CGMService.BROADCAST_NEW_CGMS_VALUE: {
CGMSRecord cgmsRecord = intent.getExtras().getParcelable(CGMService.EXTRA_CGMS_RECORD);
if (mCgmsRecordsAdapter == null) {
mCgmsRecordsAdapter = new CGMSRecordsAdapter(CGMSActivity.this);
mRecordsListView.setAdapter(mCgmsRecordsAdapter);
}
mCgmsRecordsAdapter.addItem(cgmsRecord);
mCgmsRecordsAdapter.notifyDataSetChanged();
break;
}
case CGMService.BROADCAST_DATA_SET_CLEAR:
// Update GUI
clearRecords();
break;
case CGMService.OPERATION_STARTED:
// Update GUI
setOperationInProgress(true);
break;
case CGMService.BROADCAST_BATTERY_LEVEL:
final int batteryLevel = intent.getIntExtra(CGMService.EXTRA_BATTERY_LEVEL, 0);
// Update GUI
onBatteryLevelChanged(device, batteryLevel);
break;
case CGMService.OPERATION_FAILED:
// Update GUI
showToast(R.string.gls_operation_failed);
// breakthrough intended
default:
// Update GUI
setOperationInProgress(false);
}
}
};
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(CGMService.BROADCAST_NEW_CGMS_VALUE);
intentFilter.addAction(CGMService.BROADCAST_DATA_SET_CLEAR);
intentFilter.addAction(CGMService.OPERATION_STARTED);
intentFilter.addAction(CGMService.OPERATION_COMPLETED);
intentFilter.addAction(CGMService.OPERATION_SUPPORTED);
intentFilter.addAction(CGMService.OPERATION_NOT_SUPPORTED);
intentFilter.addAction(CGMService.OPERATION_ABORTED);
intentFilter.addAction(CGMService.OPERATION_FAILED);
intentFilter.addAction(CGMService.BROADCAST_BATTERY_LEVEL);
return intentFilter;
}
}

View File

@@ -0,0 +1,445 @@
/*
* Copyright (c) 2016, 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.cgms;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import android.util.SparseArray;
import java.util.UUID;
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback;
import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureDataCallback;
import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointDataCallback;
import no.nordicsemi.android.ble.common.callback.cgm.CGMStatusDataCallback;
import no.nordicsemi.android.ble.common.callback.cgm.ContinuousGlucoseMeasurementDataCallback;
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData;
import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.CGMMeasurementParser;
import no.nordicsemi.android.nrftoolbox.parser.CGMSpecificOpsControlPointParser;
import no.nordicsemi.android.nrftoolbox.parser.RecordAccessControlPointParser;
public class CGMSManager extends BatteryManager<CGMSManagerCallbacks> {
/** Cycling Speed and Cadence service UUID. */
public static final UUID CGMS_UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb");
private static final UUID CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb");
private static final UUID CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb");
private static final UUID CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb");
private static final UUID CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb");
/** Record Access Control Point characteristic UUID. */
private static final UUID RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mCGMStatusCharacteristic;
private BluetoothGattCharacteristic mCGMFeatureCharacteristic;
private BluetoothGattCharacteristic mCGMMeasurementCharacteristic;
private BluetoothGattCharacteristic mCGMSpecificOpsControlPointCharacteristic;
private BluetoothGattCharacteristic mRecordAccessControlPointCharacteristic;
private SparseArray<CGMSRecord> mRecords = new SparseArray<>();
/** A flag set to true if the remote device supports E2E CRC. */
private boolean mSecured;
/**
* A flag set when records has been requested using RACP. This is to distinguish CGM packets
* received as continuous measurements or requested.
*/
private boolean mRecordAccessRequestInProgress;
/**
* The timestamp when the session has started. This is needed to display the user facing
* times of samples.
*/
private long mSessionStartTime;
CGMSManager(final Context context) {
super(context);
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving notification, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
// Enable Battery service
super.initialize();
// Read CGM Feature characteristic, mainly to see if the device supports E2E CRC.
// This is not supported in the experimental CGMS from the SDK.
readCharacteristic(mCGMFeatureCharacteristic)
.with(new CGMFeatureDataCallback() {
@Override
public void onContinuousGlucoseMonitorFeaturesReceived(@NonNull final BluetoothDevice device, @NonNull final CGMFeatures features,
final int type, final int sampleLocation, final boolean secured) {
mSecured = features.e2eCrcSupported;
log(LogContract.Log.Level.APPLICATION, "E2E CRC feature " + (mSecured ? "supported" : "not supported"));
}
})
.fail((device, status) -> log(Log.WARN, "Could not read CGM Feature characteristic"))
.enqueue();
// Check if the session is already started. This is not supported in the experimental CGMS from the SDK.
readCharacteristic(mCGMStatusCharacteristic)
.with(new CGMStatusDataCallback() {
@Override
public void onContinuousGlucoseMonitorStatusChanged(@NonNull final BluetoothDevice device, @NonNull final CGMStatus status, final int timeOffset, final boolean secured) {
if (!status.sessionStopped) {
mSessionStartTime = System.currentTimeMillis() - timeOffset * 60000L;
log(LogContract.Log.Level.APPLICATION, "Session already started");
}
}
})
.fail((device, status) -> log(Log.WARN, "Could not read CGM Status characteristic"))
.enqueue();
// Set notification and indication callbacks
setNotificationCallback(mCGMMeasurementCharacteristic)
.with(new ContinuousGlucoseMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + CGMMeasurementParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onContinuousGlucoseMeasurementReceived(@NonNull final BluetoothDevice device,
final float glucoseConcentration,
@Nullable final Float cgmTrend,
@Nullable final Float cgmQuality,
final CGMStatus status,
final int timeOffset,
final boolean secured) {
// If the CGM Status characteristic has not been read and the session was already started before,
// estimate the Session Start Time by subtracting timeOffset minutes from the current timestamp.
if (mSessionStartTime == 0 && !mRecordAccessRequestInProgress) {
mSessionStartTime = System.currentTimeMillis() - timeOffset * 60000L;
}
// Calculate the sample timestamp based on the Session Start Time
final long timestamp = mSessionStartTime + (timeOffset * 60000L); // Sequence number is in minutes since Start Session
final CGMSRecord record = new CGMSRecord(timeOffset, glucoseConcentration, timestamp);
mRecords.put(record.sequenceNumber, record);
mCallbacks.onCGMValueReceived(device, record);
}
@Override
public void onContinuousGlucoseMeasurementReceivedWithCrcError(@NonNull final BluetoothDevice device,
@NonNull final Data data) {
log(Log.WARN, "Continuous Glucose Measurement record received with CRC error");
}
});
setIndicationCallback(mCGMSpecificOpsControlPointCharacteristic)
.with(new CGMSpecificOpsControlPointDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + CGMSpecificOpsControlPointParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onCGMSpecificOpsOperationCompleted(@NonNull final BluetoothDevice device,
final int requestCode, final boolean secured) {
switch (requestCode) {
case CGM_OP_CODE_START_SESSION:
mSessionStartTime = System.currentTimeMillis();
break;
case CGM_OP_CODE_STOP_SESSION:
mSessionStartTime = 0;
break;
}
}
@SuppressWarnings("StatementWithEmptyBody")
@Override
public void onCGMSpecificOpsOperationError(@NonNull final BluetoothDevice device,
final int requestCode, final int errorCode,
final boolean secured) {
switch (requestCode) {
case CGM_OP_CODE_START_SESSION:
if (errorCode == CGM_ERROR_PROCEDURE_NOT_COMPLETED) {
// Session was already started before.
// Looks like the CGM Status characteristic has not been read,
// otherwise we would have got the Session Start Time before.
// The Session Start Time will be calculated when a next CGM
// packet is received based on it's Time Offset.
}
case CGM_OP_CODE_STOP_SESSION:
mSessionStartTime = 0;
break;
}
}
@Override
public void onCGMSpecificOpsResponseReceivedWithCrcError(@NonNull final BluetoothDevice device,
@NonNull final Data data) {
log(Log.ERROR, "Request failed: CRC error");
}
});
setIndicationCallback(mRecordAccessControlPointCharacteristic)
.with(new RecordAccessControlPointDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onRecordAccessOperationCompleted(@NonNull final BluetoothDevice device, final int requestCode) {
switch (requestCode) {
case RACP_OP_CODE_ABORT_OPERATION:
mCallbacks.onOperationAborted(device);
break;
default:
mRecordAccessRequestInProgress = false;
mCallbacks.onOperationCompleted(device);
break;
}
}
@Override
public void onRecordAccessOperationCompletedWithNoRecordsFound(@NonNull final BluetoothDevice device,
final int requestCode) {
mRecordAccessRequestInProgress = false;
mCallbacks.onOperationCompleted(device);
}
@Override
public void onNumberOfRecordsReceived(@NonNull final BluetoothDevice device, final int numberOfRecords) {
mCallbacks.onNumberOfRecordsRequested(device, numberOfRecords);
if (numberOfRecords > 0) {
if (mRecords.size() > 0) {
final int sequenceNumber = mRecords.keyAt(mRecords.size() - 1) + 1;
writeCharacteristic(mRecordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber))
.enqueue();
} else {
writeCharacteristic(mRecordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportAllStoredRecords())
.enqueue();
}
} else {
mRecordAccessRequestInProgress = false;
mCallbacks.onOperationCompleted(device);
}
}
@Override
public void onRecordAccessOperationError(@NonNull final BluetoothDevice device,
final int requestCode, final int errorCode) {
log(Log.WARN, "Record Access operation failed (error " + errorCode + ")");
if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
mCallbacks.onOperationNotSupported(device);
} else {
mCallbacks.onOperationFailed(device);
}
}
});
// Enable notifications and indications
enableNotifications(mCGMMeasurementCharacteristic)
.fail((device, status) -> log(Log.WARN, "Failed to enable Continuous Glucose Measurement notifications (" + status + ")"))
.enqueue();
enableIndications(mCGMSpecificOpsControlPointCharacteristic)
.fail((device, status) -> log(Log.WARN, "Failed to enable CGM Specific Ops Control Point indications notifications (" + status + ")"))
.enqueue();
enableIndications(mRecordAccessControlPointCharacteristic)
.fail((device, status) -> log(Log.WARN, "Failed to enabled Record Access Control Point indications (error " + status + ")"))
.enqueue();
// Start Continuous Glucose session if hasn't been started before
if (mSessionStartTime == 0L) {
writeCharacteristic(mCGMSpecificOpsControlPointCharacteristic, CGMSpecificOpsControlPointData.startSession(mSecured))
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + CGMSpecificOpsControlPointParser.parse(data) + "\" sent"))
.fail((device, status) -> log(LogContract.Log.Level.ERROR, "Failed to start session (error " + status + ")"))
.enqueue();
}
}
@Override
protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(CGMS_UUID);
if (service != null) {
mCGMStatusCharacteristic = service.getCharacteristic(CGM_STATUS_UUID);
mCGMFeatureCharacteristic = service.getCharacteristic(CGM_FEATURE_UUID);
mCGMMeasurementCharacteristic = service.getCharacteristic(CGM_MEASUREMENT_UUID);
mCGMSpecificOpsControlPointCharacteristic = service.getCharacteristic(CGM_OPS_CONTROL_POINT_UUID);
mRecordAccessControlPointCharacteristic = service.getCharacteristic(RACP_UUID);
}
return mCGMMeasurementCharacteristic != null
&& mCGMSpecificOpsControlPointCharacteristic != null
&& mRecordAccessControlPointCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
super.onDeviceDisconnected();
mCGMStatusCharacteristic = null;
mCGMFeatureCharacteristic = null;
mCGMMeasurementCharacteristic = null;
mCGMSpecificOpsControlPointCharacteristic = null;
mRecordAccessControlPointCharacteristic = null;
}
};
/**
* Returns a list of CGM records obtained from this device. The key in the array is the
*/
public SparseArray<CGMSRecord> getRecords() {
return mRecords;
}
/**
* Clears the records list locally
*/
public void clear() {
mRecords.clear();
mCallbacks.onDatasetCleared(getBluetoothDevice());
}
/**
* Sends the request to obtain the last (most recent) record from glucose device.
* The data will be returned to Glucose Measurement characteristic as a notification followed by
* Record Access Control Point indication with status code Success or other in case of error.
*/
public void getLastRecord() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
mRecordAccessRequestInProgress = true;
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.reportLastStoredRecord())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends the request to obtain the first (oldest) record from glucose device.
* The data will be returned to Glucose Measurement characteristic as a notification followed by
* Record Access Control Point indication with status code Success or other in case of error.
*/
public void getFirstRecord() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
mRecordAccessRequestInProgress = true;
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.reportFirstStoredRecord())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends abort operation signal to the device.
*/
public void abort() {
if (mRecordAccessControlPointCharacteristic == null)
return;
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.abortOperation())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends the request to obtain all records from glucose device. Initially we want to notify the
* user about the number of the records so the Report Number of Stored Records request is send.
* The data will be returned to Glucose Measurement characteristic as a notification followed by
* Record Access Control Point indication with status code Success or other in case of error.
*/
public void getAllRecords() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
mRecordAccessRequestInProgress = true;
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.reportNumberOfAllStoredRecords())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends the request to obtain all records from glucose device. Initially we want to notify the
* user about the number of the records so the Report Number of Stored Records request is send.
* The data will be returned to Glucose Measurement characteristic as a notification followed by
* Record Access Control Point indication with status code Success or other in case of error.
*/
public void refreshRecords() {
if (mRecordAccessControlPointCharacteristic == null)
return;
if (mRecords.size() == 0) {
getAllRecords();
} else {
mCallbacks.onOperationStarted(getBluetoothDevice());
// Obtain the last sequence number
final int sequenceNumber = mRecords.keyAt(mRecords.size() - 1) + 1;
mRecordAccessRequestInProgress = true;
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber))
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
// Info:
// Operators OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by the CGMS sample from SDK
// The "Operation not supported" response will be received
}
}
/**
* Sends the request to remove all stored records from the Continuous Glucose Monitor device.
* This feature is not supported by the CGMS sample from the SDK, so monitor will answer with
* the Op Code Not Supported error.
*/
public void deleteAllRecords() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.deleteAllStoredRecords())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2016, 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.cgms;
import android.bluetooth.BluetoothDevice;
import androidx.annotation.NonNull;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
public interface CGMSManagerCallbacks extends BatteryManagerCallbacks {
void onCGMValueReceived(@NonNull final BluetoothDevice device, final CGMSRecord record);
void onOperationStarted(final @NonNull BluetoothDevice device);
void onOperationCompleted(final @NonNull BluetoothDevice device);
void onOperationFailed(final @NonNull BluetoothDevice device);
void onOperationAborted(final @NonNull BluetoothDevice device);
void onOperationNotSupported(final @NonNull BluetoothDevice device);
void onDatasetCleared(final @NonNull BluetoothDevice device);
void onNumberOfRecordsRequested(final @NonNull BluetoothDevice device, final int value);
}

View File

@@ -0,0 +1,49 @@
package no.nordicsemi.android.nrftoolbox.cgms;
import android.os.Parcel;
import android.os.Parcelable;
class CGMSRecord implements Parcelable{
/** Record sequence number. */
protected int sequenceNumber;
/** The base time of the measurement (start time + sequenceNumber of minutes). */
protected long timestamp;
/** The glucose concentration in mg/dL. */
protected float glucoseConcentration;
CGMSRecord(final int sequenceNumber, final float glucoseConcentration, final long timestamp) {
this.sequenceNumber = sequenceNumber;
this.glucoseConcentration = glucoseConcentration;
this.timestamp = timestamp;
}
private CGMSRecord(final Parcel in) {
this.sequenceNumber = in.readInt();
this.glucoseConcentration = in.readFloat();
this.timestamp = in.readLong();
}
public static final Creator<CGMSRecord> CREATOR = new Creator<CGMSRecord>() {
@Override
public CGMSRecord createFromParcel(final Parcel in) {
return new CGMSRecord(in);
}
@Override
public CGMSRecord[] newArray(final int size) {
return new CGMSRecord[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
parcel.writeInt(sequenceNumber);
parcel.writeFloat(glucoseConcentration);
parcel.writeLong(timestamp);
}
}

View File

@@ -0,0 +1,79 @@
package no.nordicsemi.android.nrftoolbox.cgms;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import no.nordicsemi.android.nrftoolbox.R;
public class CGMSRecordsAdapter extends BaseAdapter {
private final static SimpleDateFormat mTimeFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US);
private List<CGMSRecord> mRecords;
private LayoutInflater mInflater;
public CGMSRecordsAdapter(final Context context) {
mRecords = new ArrayList<>();
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return mRecords.size();
}
@Override
public Object getItem(int i) {
return null;
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.activity_feature_cgms_item, parent, false);
viewHolder = new ViewHolder();
viewHolder.concentration = convertView.findViewById(R.id.cgms_concentration);
viewHolder.time = convertView.findViewById(R.id.time);
viewHolder.details = convertView.findViewById(R.id.details);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
final CGMSRecord cgmsRecord = mRecords.get(position);
viewHolder.concentration.setText(String.valueOf(cgmsRecord.glucoseConcentration));
viewHolder.details.setText(viewHolder.details.getResources().getString(R.string.cgms_details, cgmsRecord.sequenceNumber));
viewHolder.time.setText(mTimeFormat.format(new Date(cgmsRecord.timestamp)));
return convertView;
}
public void addItem(final CGMSRecord record) {
mRecords.add(record);
}
public void clear() {
mRecords.clear();
}
private static class ViewHolder {
TextView time;
TextView details;
TextView concentration;
}
}

View File

@@ -0,0 +1,290 @@
package no.nordicsemi.android.nrftoolbox.cgms;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.util.SparseArray;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.FeaturesActivity;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.ToolboxApplication;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileService;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
public class CGMService extends BleProfileService implements CGMSManagerCallbacks {
private static final String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.cgms.ACTION_DISCONNECT";
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
public static final String BROADCAST_NEW_CGMS_VALUE = "no.nordicsemi.android.nrftoolbox.cgms.BROADCAST_NEW_CGMS_VALUE";
public static final String BROADCAST_DATA_SET_CLEAR = "no.nordicsemi.android.nrftoolbox.cgms.BROADCAST_DATA_SET_CLEAR";
public static final String OPERATION_STARTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_STARTED";
public static final String OPERATION_COMPLETED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_COMPLETED";
public static final String OPERATION_SUPPORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_SUPPORTED";
public static final String OPERATION_NOT_SUPPORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_NOT_SUPPORTED";
public static final String OPERATION_FAILED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_FAILED";
public static final String OPERATION_ABORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_ABORTED";
public static final String EXTRA_CGMS_RECORD = "no.nordicsemi.android.nrftoolbox.cgms.EXTRA_CGMS_RECORD";
public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.cgms.EXTRA_DATA";
private final static int NOTIFICATION_ID = 229;
private final static int OPEN_ACTIVITY_REQ = 0;
private final static int DISCONNECT_REQ = 1;
private CGMSManager mManager;
private final LocalBinder mBinder = new CGMSBinder();
/**
* This local binder is an interface for the bonded activity to operate with the RSC sensor
*/
public class CGMSBinder extends LocalBinder {
/**
* Returns all records as a sparse array where sequence number is the key.
*
* @return the records list
*/
public SparseArray<CGMSRecord> getRecords() {
return mManager.getRecords();
}
/**
* Clears the records list locally
*/
public void clear() {
if (mManager != null)
mManager.clear();
}
/**
* Sends the request to obtain the first (oldest) record from glucose device.
* The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control
* Point indication with status code ({@link CGMSManager# RESPONSE_SUCCESS} or other in case of error.
*/
public void getFirstRecord() {
if (mManager != null)
mManager.getFirstRecord();
}
/**
* Sends the request to obtain the last (most recent) record from glucose device.
* The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access
* Control Point indication with status code Success or other in case of error.
*/
public void getLastRecord() {
if (mManager != null)
mManager.getLastRecord();
}
/**
* Sends the request to obtain all records from glucose device.
* Initially we want to notify user about the number of the records so the Report Number of Stored Records is send.
* The data will be returned to Glucose Measurement characteristic as a series of notifications followed
* by Record Access Control Point indication with status code Success or other in case of error.
*/
public void getAllRecords() {
if (mManager != null)
mManager.getAllRecords();
}
/**
* Sends the request to obtain all records from glucose device with sequence number greater
* than the last one already obtained. The data will be returned to Glucose Measurement
* characteristic as a series of notifications followed by Record Access Control Point
* indication with status code Success or other in case of error.
*/
public void refreshRecords() {
if (mManager != null)
mManager.refreshRecords();
}
/**
* Sends abort operation signal to the device
*/
public void abort() {
if (mManager != null)
mManager.abort();
}
/**
* Sends Delete op code with All stored records parameter. This method may not be supported by the SDK sample.
*/
public void deleteAllRecords() {
if (mManager != null)
mManager.deleteAllRecords();
}
}
@Override
protected LocalBinder getBinder() {
return mBinder;
}
@Override
protected LoggableBleManager<CGMSManagerCallbacks> initializeManager() {
return mManager = new CGMSManager(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.csc_notification_connected_message, 0);
}
/**
* Creates the notification
*
* @param messageResId the message resource id. The message must have one String parameter,<br />
* f.e. <code>&lt;string name="name"&gt;%s is connected&lt;/string&gt;</code>
* @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, CGMSActivity.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 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL);
builder.setContentIntent(pendingIntent);
builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName()));
builder.setSmallIcon(R.drawable.ic_stat_notify_cgms);
builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true);
builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_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();
}
};
@Override
public void onCGMValueReceived(@NonNull final BluetoothDevice device, final CGMSRecord record) {
final Intent broadcast = new Intent(BROADCAST_NEW_CGMS_VALUE);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_CGMS_RECORD, record);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onOperationStarted(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(OPERATION_STARTED);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_DATA, true);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onOperationCompleted(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(OPERATION_COMPLETED);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_DATA, true);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onOperationFailed(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(OPERATION_FAILED);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_DATA, true);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onOperationAborted(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(OPERATION_ABORTED);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_DATA, true);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onOperationNotSupported(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(OPERATION_NOT_SUPPORTED);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_DATA, false);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDatasetCleared(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_DATA_SET_CLEAR);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onNumberOfRecordsRequested(@NonNull final BluetoothDevice device, final int value) {
if (value == 0)
showToast(R.string.gls_progress_zero);
else
showToast(getResources().getQuantityString(R.plurals.gls_progress, value, value));
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
}

View File

@@ -0,0 +1,267 @@
/*
* 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.csc;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.view.Menu;
import android.widget.TextView;
import java.util.Locale;
import java.util.UUID;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.csc.settings.SettingsActivity;
import no.nordicsemi.android.nrftoolbox.csc.settings.SettingsFragment;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileService;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity;
public class CSCActivity extends BleProfileServiceReadyActivity<CSCService.CSCBinder> {
private TextView mSpeedView;
private TextView mSpeedUnitView;
private TextView mCadenceView;
private TextView mDistanceView;
private TextView mDistanceUnitView;
private TextView mTotalDistanceView;
private TextView mTotalDistanceUnitView;
private TextView mGearRatioView;
private TextView mBatteryLevelView;
@Override
protected void onCreateView(final Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_csc);
setGui();
}
@Override
protected void onInitialize(final Bundle savedInstanceState) {
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, makeIntentFilter());
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
}
private void setGui() {
mSpeedView = findViewById(R.id.speed);
mSpeedUnitView = findViewById(R.id.speed_unit);
mCadenceView = findViewById(R.id.cadence);
mDistanceView = findViewById(R.id.distance);
mDistanceUnitView = findViewById(R.id.distance_unit);
mTotalDistanceView = findViewById(R.id.distance_total);
mTotalDistanceUnitView = findViewById(R.id.distance_total_unit);
mGearRatioView = findViewById(R.id.ratio);
mBatteryLevelView = findViewById(R.id.battery);
}
@Override
protected void onResume() {
super.onResume();
setDefaultUI();
}
@Override
protected void setDefaultUI() {
mSpeedView.setText(R.string.not_available_value);
mCadenceView.setText(R.string.not_available_value);
mDistanceView.setText(R.string.not_available_value);
mTotalDistanceView.setText(R.string.not_available_value);
mGearRatioView.setText(R.string.not_available_value);
mBatteryLevelView.setText(R.string.not_available);
setUnits();
}
private void setUnits() {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT)));
switch (unit) {
case SettingsFragment.SETTINGS_UNIT_M_S: // [m/s]
mSpeedUnitView.setText(R.string.csc_speed_unit_m_s);
mDistanceUnitView.setText(R.string.csc_distance_unit_m);
mTotalDistanceUnitView.setText(R.string.csc_total_distance_unit_km);
break;
case SettingsFragment.SETTINGS_UNIT_KM_H: // [km/h]
mSpeedUnitView.setText(R.string.csc_speed_unit_km_h);
mDistanceUnitView.setText(R.string.csc_distance_unit_m);
mTotalDistanceUnitView.setText(R.string.csc_total_distance_unit_km);
break;
case SettingsFragment.SETTINGS_UNIT_MPH: // [mph]
mSpeedUnitView.setText(R.string.csc_speed_unit_mph);
mDistanceUnitView.setText(R.string.csc_distance_unit_yd);
mTotalDistanceUnitView.setText(R.string.csc_total_distance_unit_mile);
break;
}
}
@Override
protected int getLoggerProfileTitle() {
return R.string.csc_feature_title;
}
@Override
protected int getDefaultDeviceName() {
return R.string.csc_default_name;
}
@Override
protected int getAboutTextId() {
return R.string.csc_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 Class<? extends BleProfileService> getServiceClass() {
return CSCService.class;
}
@Override
protected UUID getFilterUUID() {
return CSCManager.CYCLING_SPEED_AND_CADENCE_SERVICE_UUID;
}
@Override
protected void onServiceBound(final CSCService.CSCBinder binder) {
// not used
}
@Override
protected void onServiceUnbound() {
// not used
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
// not used
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
super.onDeviceDisconnected(device);
mBatteryLevelView.setText(R.string.not_available);
}
private void onMeasurementReceived(final BluetoothDevice device, float speed, float distance, float totalDistance) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT)));
switch (unit) {
case SettingsFragment.SETTINGS_UNIT_KM_H:
speed = speed * 3.6f;
// pass through intended
case SettingsFragment.SETTINGS_UNIT_M_S:
if (distance < 1000) { // 1 km in m
mDistanceView.setText(String.format(Locale.US, "%.0f", distance));
mDistanceUnitView.setText(R.string.csc_distance_unit_m);
} else {
mDistanceView.setText(String.format(Locale.US, "%.2f", distance / 1000.0f));
mDistanceUnitView.setText(R.string.csc_distance_unit_km);
}
mTotalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1000.0f));
break;
case SettingsFragment.SETTINGS_UNIT_MPH:
speed = speed * 2.2369f;
if (distance < 1760) { // 1 mile in yrs
mDistanceView.setText(String.format(Locale.US, "%.0f", distance));
mDistanceUnitView.setText(R.string.csc_distance_unit_yd);
} else {
mDistanceView.setText(String.format(Locale.US, "%.2f", distance / 1760.0f));
mDistanceUnitView.setText(R.string.csc_distance_unit_mile);
}
mTotalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1609.31f));
break;
}
mSpeedView.setText(String.format(Locale.US, "%.1f", speed));
}
private void onGearRatioUpdate(final BluetoothDevice device, final int cadence, final float ratio) {
mCadenceView.setText(String.format(Locale.US, "%d", cadence));
mGearRatioView.setText(String.format(Locale.US, "%.1f", ratio));
}
public void onBatteryLevelChanged(final BluetoothDevice device, final int value) {
mBatteryLevelView.setText(getString(R.string.battery, value));
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
final BluetoothDevice device = intent.getParcelableExtra(CSCService.EXTRA_DEVICE);
if (CSCService.BROADCAST_WHEEL_DATA.equals(action)) {
final float speed = intent.getFloatExtra(CSCService.EXTRA_SPEED, 0.0f); // [m/s]
final float distance = intent.getFloatExtra(CSCService.EXTRA_DISTANCE, 0);
final float totalDistance = intent.getFloatExtra(CSCService.EXTRA_TOTAL_DISTANCE, 0);
// Update GUI
onMeasurementReceived(device, speed, distance, totalDistance);
} else if (CSCService.BROADCAST_CRANK_DATA.equals(action)) {
final float ratio = intent.getFloatExtra(CSCService.EXTRA_GEAR_RATIO, 0);
final int cadence = intent.getIntExtra(CSCService.EXTRA_CADENCE, 0);
// Update GUI
onGearRatioUpdate(device, cadence, ratio);
} else if (CSCService.BROADCAST_BATTERY_LEVEL.equals(action)) {
final int batteryLevel = intent.getIntExtra(CSCService.EXTRA_BATTERY_LEVEL, 0);
// Update GUI
onBatteryLevelChanged(device, batteryLevel);
}
}
};
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(CSCService.BROADCAST_WHEEL_DATA);
intentFilter.addAction(CSCService.BROADCAST_CRANK_DATA);
intentFilter.addAction(CSCService.BROADCAST_BATTERY_LEVEL);
return intentFilter;
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.csc;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import android.util.Log;
import java.util.UUID;
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.csc.settings.SettingsFragment;
import no.nordicsemi.android.nrftoolbox.parser.CSCMeasurementParser;
public class CSCManager extends BatteryManager<CSCManagerCallbacks> {
/** Cycling Speed and Cadence service UUID. */
public final static UUID CYCLING_SPEED_AND_CADENCE_SERVICE_UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb");
/** Cycling Speed and Cadence Measurement characteristic UUID. */
private final static UUID CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb");
private final SharedPreferences preferences;
private BluetoothGattCharacteristic mCSCMeasurementCharacteristic;
CSCManager(final Context context) {
super(context);
preferences = PreferenceManager.getDefaultSharedPreferences(context);
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
super.initialize();
// CSC characteristic is required
setNotificationCallback(mCSCMeasurementCharacteristic)
.with(new CyclingSpeedAndCadenceMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, final @NonNull Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + CSCMeasurementParser.parse(data) + "\" received");
// Pass through received data
super.onDataReceived(device, data);
}
@Override
public float getWheelCircumference() {
return Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_WHEEL_SIZE,
String.valueOf(SettingsFragment.SETTINGS_WHEEL_SIZE_DEFAULT)));
}
@Override
public void onDistanceChanged(@NonNull final BluetoothDevice device,
final float totalDistance, final float distance, final float speed) {
mCallbacks.onDistanceChanged(device, totalDistance, distance, speed);
}
@Override
public void onCrankDataChanged(@NonNull final BluetoothDevice device,
final float crankCadence, final float gearRatio) {
mCallbacks.onCrankDataChanged(device, crankCadence, gearRatio);
}
@Override
public void onInvalidDataReceived(@NonNull final BluetoothDevice device,
final @NonNull Data data) {
log(Log.WARN, "Invalid CSC Measurement data received: " + data);
}
});
enableNotifications(mCSCMeasurementCharacteristic).enqueue();
}
@Override
public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(CYCLING_SPEED_AND_CADENCE_SERVICE_UUID);
if (service != null) {
mCSCMeasurementCharacteristic = service.getCharacteristic(CSC_MEASUREMENT_CHARACTERISTIC_UUID);
}
return mCSCMeasurementCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
super.onDeviceDisconnected();
mCSCMeasurementCharacteristic = null;
}
};
}

View File

@@ -0,0 +1,28 @@
/*
* 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.csc;
import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
interface CSCManagerCallbacks extends BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback {
}

View File

@@ -0,0 +1,209 @@
/*
* 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.csc;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.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.ToolboxApplication;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileService;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
public class CSCService extends BleProfileService implements CSCManagerCallbacks {
@SuppressWarnings("unused")
private static final String TAG = "CSCService";
public static final String BROADCAST_WHEEL_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_WHEEL_DATA";
/** Speed in meters per second. */
public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_SPEED";
/** Distance in meters. */
public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_DISTANCE";
/** Total distance in meters. */
public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_TOTAL_DISTANCE";
public static final String BROADCAST_CRANK_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_CRANK_DATA";
public static final String EXTRA_GEAR_RATIO = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_GEAR_RATIO";
public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_CADENCE";
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
private static final String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.csc.ACTION_DISCONNECT";
private final static int NOTIFICATION_ID = 200;
private final static int OPEN_ACTIVITY_REQ = 0;
private final static int DISCONNECT_REQ = 1;
private final LocalBinder mBinder = new CSCBinder();
private CSCManager mManager;
/**
* This local binder is an interface for the bonded activity to operate with the RSC sensor
*/
class CSCBinder extends LocalBinder {
// empty
}
@Override
protected LocalBinder getBinder() {
return mBinder;
}
@Override
protected LoggableBleManager<CSCManagerCallbacks> initializeManager() {
return mManager = new CSCManager(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();
if (isConnected()) {
// This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property).
// If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications.
mManager.readBatteryLevelCharacteristic();
}
}
@Override
protected void onUnbind() {
// When we are connected, but the application is not open, we are not really interested in battery level notifications.
// But we will still be receiving other values, if enabled.
if (isConnected())
mManager.disableBatteryLevelCharacteristicNotifications();
// when the activity closes we need to show the notification that user is connected to the sensor
createNotification(R.string.csc_notification_connected_message, 0);
}
@Override
public void onDistanceChanged(@NonNull final BluetoothDevice device, final float totalDistance, final float distance, final float speed) {
final Intent broadcast = new Intent(BROADCAST_WHEEL_DATA);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_SPEED, speed);
broadcast.putExtra(EXTRA_DISTANCE, distance);
broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onCrankDataChanged(@NonNull final BluetoothDevice device, final float crankCadence, final float gearRatio) {
final Intent broadcast = new Intent(BROADCAST_CRANK_DATA);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_GEAR_RATIO, gearRatio);
broadcast.putExtra(EXTRA_CADENCE, (int) crankCadence);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
/**
* Creates the notification
*
* @param messageResId
* the message resource id. The message must have one String parameter,<br />
* f.e. <code>&lt;string name="name"&gt;%s is connected&lt;/string&gt;</code>
* @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, CSCActivity.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 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL);
builder.setContentIntent(pendingIntent);
builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName()));
builder.setSmallIcon(R.drawable.ic_stat_notify_csc);
builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true);
builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_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();
}
};
}

View File

@@ -0,0 +1,56 @@
/*
* 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.csc.settings;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Display the fragment as the main content.
getSupportFragmentManager().beginTransaction().replace(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);
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.csc.settings;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceFragmentCompat;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String SETTINGS_WHEEL_SIZE = "settings_wheel_size";
public static final int SETTINGS_WHEEL_SIZE_DEFAULT = 2340;
public static final String SETTINGS_UNIT = "settings_csc_unit";
public static final int SETTINGS_UNIT_M_S = 0; // [m/s]
public static final int SETTINGS_UNIT_KM_H = 1; // [m/s]
public static final int SETTINGS_UNIT_MPH = 2; // [m/s]
public static final int SETTINGS_UNIT_DEFAULT = SETTINGS_UNIT_KM_H;
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.settings_csc);
// set initial values
updateWheelSizeSummary();
}
@Override
public void onResume() {
super.onResume();
// attach the preference change listener. It will update the summary below interval preference
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
super.onPause();
// unregister listener
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if (SETTINGS_WHEEL_SIZE.equals(key)) {
updateWheelSizeSummary();
}
}
private void updateWheelSizeSummary() {
final PreferenceScreen screen = getPreferenceScreen();
final SharedPreferences preferences = getPreferenceManager().getSharedPreferences();
final String value = preferences.getString(SETTINGS_WHEEL_SIZE, String.valueOf(SETTINGS_WHEEL_SIZE_DEFAULT));
screen.findPreference(SETTINGS_WHEEL_SIZE).setSummary(getString(R.string.csc_settings_wheel_diameter_summary, value));
}
}

View File

@@ -0,0 +1,862 @@
/*
* 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.dfu;
import android.Manifest;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.LoaderManager.LoaderCallbacks;
import android.app.NotificationManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.DialogFragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import no.nordicsemi.android.dfu.DfuProgressListener;
import no.nordicsemi.android.dfu.DfuProgressListenerAdapter;
import no.nordicsemi.android.dfu.DfuServiceInitiator;
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;
import no.nordicsemi.android.nrftoolbox.AppHelpFragment;
import no.nordicsemi.android.nrftoolbox.PermissionRationaleFragment;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.dfu.adapter.FileBrowserAppsAdapter;
import no.nordicsemi.android.nrftoolbox.dfu.fragment.UploadCancelFragment;
import no.nordicsemi.android.nrftoolbox.dfu.fragment.ZipInfoFragment;
import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsActivity;
import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsFragment;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;
import no.nordicsemi.android.nrftoolbox.utility.FileHelper;
/**
* DfuActivity is the main DFU activity It implements DFUManagerCallbacks to receive callbacks from DFUManager class It implements
* DeviceScannerFragment.OnDeviceSelectedListener callback to receive callback when device is selected from scanning dialog The activity supports portrait and
* landscape orientations
*/
public class DfuActivity extends AppCompatActivity implements LoaderCallbacks<Cursor>, ScannerFragment.OnDeviceSelectedListener,
UploadCancelFragment.CancelFragmentListener, PermissionRationaleFragment.PermissionDialogListener {
private static final String TAG = "DfuActivity";
private static final String PREFS_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_DEVICE_NAME";
private static final String PREFS_FILE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_NAME";
private static final String PREFS_FILE_TYPE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_TYPE";
private static final String PREFS_FILE_SCOPE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_SCOPE";
private static final String PREFS_FILE_SIZE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_SIZE";
private static final String DATA_DEVICE = "device";
private static final String DATA_FILE_TYPE = "file_type";
private static final String DATA_FILE_TYPE_TMP = "file_type_tmp";
private static final String DATA_FILE_PATH = "file_path";
private static final String DATA_FILE_STREAM = "file_stream";
private static final String DATA_INIT_FILE_PATH = "init_file_path";
private static final String DATA_INIT_FILE_STREAM = "init_file_stream";
private static final String DATA_STATUS = "status";
private static final String DATA_SCOPE = "scope";
private static final String DATA_DFU_COMPLETED = "dfu_completed";
private static final String DATA_DFU_ERROR = "dfu_error";
private static final String EXTRA_URI = "uri";
private static final int PERMISSION_REQ = 25;
private static final int ENABLE_BT_REQ = 0;
private static final int SELECT_FILE_REQ = 1;
private static final int SELECT_INIT_FILE_REQ = 2;
private TextView mDeviceNameView;
private TextView mFileNameView;
private TextView mFileTypeView;
private TextView mFileScopeView;
private TextView mFileSizeView;
private TextView mFileStatusView;
private TextView mTextPercentage;
private TextView mTextUploading;
private ProgressBar mProgressBar;
private Button mSelectFileButton, mUploadButton, mConnectButton;
private BluetoothDevice mSelectedDevice;
private String mFilePath;
private Uri mFileStreamUri;
private String mInitFilePath;
private Uri mInitFileStreamUri;
private int mFileType;
private int mFileTypeTmp; // This value is being used when user is selecting a file not to overwrite the old value (in case he/she will cancel selecting file)
private Integer mScope;
private boolean mStatusOk;
/** Flag set to true in {@link #onRestart()} and to false in {@link #onPause()}. */
private boolean mResumed;
/** Flag set to true if DFU operation was completed while {@link #mResumed} was false. */
private boolean mDfuCompleted;
/** The error message received from DFU service while {@link #mResumed} was false. */
private String mDfuError;
/**
* The progress listener receives events from the DFU Service.
* If is registered in onCreate() and unregistered in onDestroy() so methods here may also be called
* when the screen is locked or the app went to the background. This is because the UI needs to have the
* correct information after user comes back to the activity and this information can't be read from the service
* as it might have been killed already (DFU completed or finished with error).
*/
private final DfuProgressListener mDfuProgressListener = new DfuProgressListenerAdapter() {
@Override
public void onDeviceConnecting(final String deviceAddress) {
mProgressBar.setIndeterminate(true);
mTextPercentage.setText(R.string.dfu_status_connecting);
}
@Override
public void onDfuProcessStarting(final String deviceAddress) {
mProgressBar.setIndeterminate(true);
mTextPercentage.setText(R.string.dfu_status_starting);
}
@Override
public void onEnablingDfuMode(final String deviceAddress) {
mProgressBar.setIndeterminate(true);
mTextPercentage.setText(R.string.dfu_status_switching_to_dfu);
}
@Override
public void onFirmwareValidating(final String deviceAddress) {
mProgressBar.setIndeterminate(true);
mTextPercentage.setText(R.string.dfu_status_validating);
}
@Override
public void onDeviceDisconnecting(final String deviceAddress) {
mProgressBar.setIndeterminate(true);
mTextPercentage.setText(R.string.dfu_status_disconnecting);
}
@Override
public void onDfuCompleted(final String deviceAddress) {
mTextPercentage.setText(R.string.dfu_status_completed);
if (mResumed) {
// let's wait a bit until we cancel the notification. When canceled immediately it will be recreated by service again.
new Handler().postDelayed(() -> {
onTransferCompleted();
// if this activity is still open and upload process was completed, cancel the notification
final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(DfuService.NOTIFICATION_ID);
}, 200);
} else {
// Save that the DFU process has finished
mDfuCompleted = true;
}
}
@Override
public void onDfuAborted(final String deviceAddress) {
mTextPercentage.setText(R.string.dfu_status_aborted);
// let's wait a bit until we cancel the notification. When canceled immediately it will be recreated by service again.
new Handler().postDelayed(() -> {
onUploadCanceled();
// if this activity is still open and upload process was completed, cancel the notification
final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(DfuService.NOTIFICATION_ID);
}, 200);
}
@Override
public void onProgressChanged(final String deviceAddress, final int percent, final float speed, final float avgSpeed, final int currentPart, final int partsTotal) {
mProgressBar.setIndeterminate(false);
mProgressBar.setProgress(percent);
mTextPercentage.setText(getString(R.string.dfu_uploading_percentage, percent));
if (partsTotal > 1)
mTextUploading.setText(getString(R.string.dfu_status_uploading_part, currentPart, partsTotal));
else
mTextUploading.setText(R.string.dfu_status_uploading);
}
@Override
public void onError(final String deviceAddress, final int error, final int errorType, final String message) {
if (mResumed) {
showErrorMessage(message);
// We have to wait a bit before canceling notification. This is called before DfuService creates the last notification.
new Handler().postDelayed(() -> {
// if this activity is still open and upload process was completed, cancel the notification
final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(DfuService.NOTIFICATION_ID);
}, 200);
} else {
mDfuError = message;
}
}
};
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_feature_dfu);
isBLESupported();
if (!isBLEEnabled()) {
showBLEDialog();
}
setGUI();
// Try to create sample files
if (FileHelper.newSamplesAvailable(this)) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
FileHelper.createSamples(this);
} else {
final DialogFragment dialog = PermissionRationaleFragment.getInstance(R.string.permission_sd_text, Manifest.permission.WRITE_EXTERNAL_STORAGE);
dialog.show(getSupportFragmentManager(), null);
}
}
// restore saved state
mFileType = DfuService.TYPE_AUTO; // Default
if (savedInstanceState != null) {
mFileType = savedInstanceState.getInt(DATA_FILE_TYPE);
mFileTypeTmp = savedInstanceState.getInt(DATA_FILE_TYPE_TMP);
mFilePath = savedInstanceState.getString(DATA_FILE_PATH);
mFileStreamUri = savedInstanceState.getParcelable(DATA_FILE_STREAM);
mInitFilePath = savedInstanceState.getString(DATA_INIT_FILE_PATH);
mInitFileStreamUri = savedInstanceState.getParcelable(DATA_INIT_FILE_STREAM);
mSelectedDevice = savedInstanceState.getParcelable(DATA_DEVICE);
mStatusOk = mStatusOk || savedInstanceState.getBoolean(DATA_STATUS);
mScope = savedInstanceState.containsKey(DATA_SCOPE) ? savedInstanceState.getInt(DATA_SCOPE) : null;
mUploadButton.setEnabled(mSelectedDevice != null && mStatusOk);
mDfuCompleted = savedInstanceState.getBoolean(DATA_DFU_COMPLETED);
mDfuError = savedInstanceState.getString(DATA_DFU_ERROR);
}
DfuServiceListenerHelper.registerProgressListener(this, mDfuProgressListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
DfuServiceListenerHelper.unregisterProgressListener(this, mDfuProgressListener);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(DATA_FILE_TYPE, mFileType);
outState.putInt(DATA_FILE_TYPE_TMP, mFileTypeTmp);
outState.putString(DATA_FILE_PATH, mFilePath);
outState.putParcelable(DATA_FILE_STREAM, mFileStreamUri);
outState.putString(DATA_INIT_FILE_PATH, mInitFilePath);
outState.putParcelable(DATA_INIT_FILE_STREAM, mInitFileStreamUri);
outState.putParcelable(DATA_DEVICE, mSelectedDevice);
outState.putBoolean(DATA_STATUS, mStatusOk);
if (mScope != null) outState.putInt(DATA_SCOPE, mScope);
outState.putBoolean(DATA_DFU_COMPLETED, mDfuCompleted);
outState.putString(DATA_DFU_ERROR, mDfuError);
}
private void setGUI() {
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mDeviceNameView = findViewById(R.id.device_name);
mFileNameView = findViewById(R.id.file_name);
mFileTypeView = findViewById(R.id.file_type);
mFileScopeView = findViewById(R.id.file_scope);
mFileSizeView = findViewById(R.id.file_size);
mFileStatusView = findViewById(R.id.file_status);
mSelectFileButton = findViewById(R.id.action_select_file);
mUploadButton = findViewById(R.id.action_upload);
mConnectButton = findViewById(R.id.action_connect);
mTextPercentage = findViewById(R.id.textviewProgress);
mTextUploading = findViewById(R.id.textviewUploading);
mProgressBar = findViewById(R.id.progressbar_file);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
if (isDfuServiceRunning()) {
// Restore image file information
mDeviceNameView.setText(preferences.getString(PREFS_DEVICE_NAME, ""));
mFileNameView.setText(preferences.getString(PREFS_FILE_NAME, ""));
mFileTypeView.setText(preferences.getString(PREFS_FILE_TYPE, ""));
mFileScopeView.setText(preferences.getString(PREFS_FILE_SCOPE, ""));
mFileSizeView.setText(preferences.getString(PREFS_FILE_SIZE, ""));
mFileStatusView.setText(R.string.dfu_file_status_ok);
mStatusOk = true;
showProgressBar();
}
}
@Override
protected void onResume() {
super.onResume();
mResumed = true;
if (mDfuCompleted)
onTransferCompleted();
if (mDfuError != null)
showErrorMessage(mDfuError);
if (mDfuCompleted || mDfuError != null) {
// if this activity is still open and upload process was completed, cancel the notification
final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(DfuService.NOTIFICATION_ID);
mDfuCompleted = false;
mDfuError = null;
}
}
@Override
protected void onPause() {
super.onPause();
mResumed = false;
}
@Override
public void onRequestPermission(final String permission) {
ActivityCompat.requestPermissions(this, new String[] { permission }, PERMISSION_REQ);
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PERMISSION_REQ: {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// We have been granted the Manifest.permission.WRITE_EXTERNAL_STORAGE permission. Now we may proceed with exporting.
FileHelper.createSamples(this);
} else {
Toast.makeText(this, R.string.no_required_permission, Toast.LENGTH_SHORT).show();
}
break;
}
}
}
private void isBLESupported() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
showToast(R.string.no_ble);
finish();
}
}
private boolean isBLEEnabled() {
final BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
final BluetoothAdapter adapter = manager.getAdapter();
return adapter != null && adapter.isEnabled();
}
private void showBLEDialog() {
final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, ENABLE_BT_REQ);
}
private void showDeviceScanningDialog() {
final ScannerFragment dialog = ScannerFragment.getInstance(null); // Device that is advertising directly does not have the GENERAL_DISCOVERABLE nor LIMITED_DISCOVERABLE flag set.
dialog.show(getSupportFragmentManager(), "scan_fragment");
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.settings_and_about, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.action_about:
final AppHelpFragment fragment = AppHelpFragment.getInstance(R.string.dfu_about_text);
fragment.show(getSupportFragmentManager(), "help_fragment");
break;
case R.id.action_settings:
final Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
break;
}
return true;
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
if (resultCode != RESULT_OK)
return;
switch (requestCode) {
case SELECT_FILE_REQ: {
// clear previous data
mFileType = mFileTypeTmp;
mFilePath = null;
mFileStreamUri = null;
// and read new one
final Uri uri = data.getData();
/*
* The URI returned from application may be in 'file' or 'content' schema. 'File' schema allows us to create a File object and read details from if
* directly. Data from 'Content' schema must be read by Content Provider. To do that we are using a Loader.
*/
if (uri.getScheme().equals("file")) {
// the direct path to the file has been returned
final String path = uri.getPath();
final File file = new File(path);
mFilePath = path;
updateFileInfo(file.getName(), file.length(), mFileType);
} else if (uri.getScheme().equals("content")) {
// an Uri has been returned
mFileStreamUri = uri;
// if application returned Uri for streaming, let's us it. Does it works?
// FIXME both Uris works with Google Drive app. Why both? What's the difference? How about other apps like DropBox?
final Bundle extras = data.getExtras();
if (extras != null && extras.containsKey(Intent.EXTRA_STREAM))
mFileStreamUri = extras.getParcelable(Intent.EXTRA_STREAM);
// file name and size must be obtained from Content Provider
final Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_URI, uri);
getLoaderManager().restartLoader(SELECT_FILE_REQ, bundle, this);
}
break;
}
case SELECT_INIT_FILE_REQ: {
mInitFilePath = null;
mInitFileStreamUri = null;
// and read new one
final Uri uri = data.getData();
/*
* The URI returned from application may be in 'file' or 'content' schema. 'File' schema allows us to create a File object and read details from if
* directly. Data from 'Content' schema must be read by Content Provider. To do that we are using a Loader.
*/
if (uri.getScheme().equals("file")) {
// the direct path to the file has been returned
mInitFilePath = uri.getPath();
mFileStatusView.setText(R.string.dfu_file_status_ok_with_init);
} else if (uri.getScheme().equals("content")) {
// an Uri has been returned
mInitFileStreamUri = uri;
// if application returned Uri for streaming, let's us it. Does it works?
// FIXME both Uris works with Google Drive app. Why both? What's the difference? How about other apps like DropBox?
final Bundle extras = data.getExtras();
if (extras != null && extras.containsKey(Intent.EXTRA_STREAM))
mInitFileStreamUri = extras.getParcelable(Intent.EXTRA_STREAM);
mFileStatusView.setText(R.string.dfu_file_status_ok_with_init);
}
break;
}
default:
break;
}
}
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
final Uri uri = args.getParcelable(EXTRA_URI);
/*
* Some apps, f.e. Google Drive allow to select file that is not on the device. There is no "_data" column handled by that provider. Let's try to obtain
* all columns and than check which columns are present.
*/
// final String[] projection = new String[] { MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.DATA };
return new CursorLoader(this, uri, null /* all columns, instead of projection */, null, null, null);
}
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
mFileNameView.setText(null);
mFileTypeView.setText(null);
mFileSizeView.setText(null);
mFilePath = null;
mFileStreamUri = null;
mStatusOk = false;
}
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
if (data != null && data.moveToNext()) {
/*
* Here we have to check the column indexes by name as we have requested for all. The order may be different.
*/
final String fileName = data.getString(data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)/* 0 DISPLAY_NAME */);
final int fileSize = data.getInt(data.getColumnIndex(MediaStore.MediaColumns.SIZE) /* 1 SIZE */);
String filePath = null;
final int dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA);
if (dataIndex != -1)
filePath = data.getString(dataIndex /* 2 DATA */);
if (!TextUtils.isEmpty(filePath))
mFilePath = filePath;
updateFileInfo(fileName, fileSize, mFileType);
} else {
mFileNameView.setText(null);
mFileTypeView.setText(null);
mFileSizeView.setText(null);
mFilePath = null;
mFileStreamUri = null;
mFileStatusView.setText(R.string.dfu_file_status_error);
mStatusOk = false;
}
}
/**
* Updates the file information on UI
*
* @param fileName file name
* @param fileSize file length
*/
private void updateFileInfo(final String fileName, final long fileSize, final int fileType) {
mFileNameView.setText(fileName);
switch (fileType) {
case DfuService.TYPE_AUTO:
mFileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[0]);
break;
case DfuService.TYPE_SOFT_DEVICE:
mFileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[1]);
break;
case DfuService.TYPE_BOOTLOADER:
mFileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[2]);
break;
case DfuService.TYPE_APPLICATION:
mFileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[3]);
break;
}
mFileSizeView.setText(getString(R.string.dfu_file_size_text, fileSize));
mFileScopeView.setText(getString(R.string.not_available));
final String extension = mFileType == DfuService.TYPE_AUTO ? "(?i)ZIP" : "(?i)HEX|BIN"; // (?i) = case insensitive
final boolean statusOk = mStatusOk = MimeTypeMap.getFileExtensionFromUrl(fileName).matches(extension);
mFileStatusView.setText(statusOk ? R.string.dfu_file_status_ok : R.string.dfu_file_status_invalid);
mUploadButton.setEnabled(mSelectedDevice != null && statusOk);
// Ask the user for the Init packet file if HEX or BIN files are selected. In case of a ZIP file the Init packets should be included in the ZIP.
if (statusOk) {
if (fileType != DfuService.TYPE_AUTO) {
mScope = null;
mFileScopeView.setText(getString(R.string.not_available));
new AlertDialog.Builder(this).setTitle(R.string.dfu_file_init_title).setMessage(R.string.dfu_file_init_message)
.setNegativeButton(R.string.no, (dialog, which) -> {
mInitFilePath = null;
mInitFileStreamUri = null;
}).setPositiveButton(R.string.yes, (dialog, which) -> {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(DfuService.MIME_TYPE_OCTET_STREAM);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, SELECT_INIT_FILE_REQ);
}).show();
} else {
new AlertDialog.Builder(this).setTitle(R.string.dfu_file_scope_title).setCancelable(false)
.setSingleChoiceItems(R.array.dfu_file_scope, 0, (dialog, which) -> {
switch (which) {
case 0:
mScope = null;
break;
case 1:
mScope = DfuServiceInitiator.SCOPE_SYSTEM_COMPONENTS;
break;
case 2:
mScope = DfuServiceInitiator.SCOPE_APPLICATION;
break;
}
}).setPositiveButton(R.string.ok, (dialogInterface, i) -> {
int index;
if (mScope == null) {
index = 0;
} else if (mScope == DfuServiceInitiator.SCOPE_SYSTEM_COMPONENTS) {
index = 1;
} else {
index = 2;
}
mFileScopeView.setText(getResources().getStringArray(R.array.dfu_file_scope)[index]);
}).show();
}
}
}
/**
* Called when the question mark was pressed
*
* @param view a button that was pressed
*/
public void onSelectFileHelpClicked(final View view) {
new AlertDialog.Builder(this).setTitle(R.string.dfu_help_title).setMessage(R.string.dfu_help_message).setPositiveButton(R.string.ok, null)
.show();
}
/**
* Called when Select File was pressed
*
* @param view a button that was pressed
*/
public void onSelectFileClicked(final View view) {
mFileTypeTmp = mFileType;
int index = 0;
switch (mFileType) {
case DfuService.TYPE_AUTO:
index = 0;
break;
case DfuService.TYPE_SOFT_DEVICE:
index = 1;
break;
case DfuService.TYPE_BOOTLOADER:
index = 2;
break;
case DfuService.TYPE_APPLICATION:
index = 3;
break;
}
// Show a dialog with file types
new AlertDialog.Builder(this).setTitle(R.string.dfu_file_type_title)
.setSingleChoiceItems(R.array.dfu_file_type, index, (dialog, which) -> {
switch (which) {
case 0:
mFileTypeTmp = DfuService.TYPE_AUTO;
break;
case 1:
mFileTypeTmp = DfuService.TYPE_SOFT_DEVICE;
break;
case 2:
mFileTypeTmp = DfuService.TYPE_BOOTLOADER;
break;
case 3:
mFileTypeTmp = DfuService.TYPE_APPLICATION;
break;
}
}).setPositiveButton(R.string.ok, (dialog, which) -> openFileChooser()).setNeutralButton(R.string.dfu_file_info, (dialog, which) -> {
final ZipInfoFragment fragment = new ZipInfoFragment();
fragment.show(getSupportFragmentManager(), "help_fragment");
}).setNegativeButton(R.string.cancel, null).show();
}
private void openFileChooser() {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(mFileTypeTmp == DfuService.TYPE_AUTO ? DfuService.MIME_TYPE_ZIP : DfuService.MIME_TYPE_OCTET_STREAM);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (intent.resolveActivity(getPackageManager()) != null) {
// file browser has been found on the device
startActivityForResult(intent, SELECT_FILE_REQ);
} else {
// there is no any file browser app, let's try to download one
final View customView = getLayoutInflater().inflate(R.layout.app_file_browser, null);
final ListView appsList = customView.findViewById(android.R.id.list);
appsList.setAdapter(new FileBrowserAppsAdapter(this));
appsList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
appsList.setItemChecked(0, true);
new AlertDialog.Builder(this).setTitle(R.string.dfu_alert_no_filebrowser_title).setView(customView)
.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss()).setPositiveButton(R.string.ok, (dialog, which) -> {
final int pos = appsList.getCheckedItemPosition();
if (pos >= 0) {
final String query = getResources().getStringArray(R.array.dfu_app_file_browser_action)[pos];
final Intent storeIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(query));
startActivity(storeIntent);
}
}).show();
}
}
/**
* Callback of UPDATE/CANCEL button on DfuActivity
*/
public void onUploadClicked(final View view) {
if (isDfuServiceRunning()) {
showUploadCancelDialog();
return;
}
// Check whether the selected file is a HEX file (we are just checking the extension)
if (!mStatusOk) {
Toast.makeText(this, R.string.dfu_file_status_invalid_message, Toast.LENGTH_LONG).show();
return;
}
// Save current state in order to restore it if user quit the Activity
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final SharedPreferences.Editor editor = preferences.edit();
editor.putString(PREFS_DEVICE_NAME, mSelectedDevice.getName());
editor.putString(PREFS_FILE_NAME, mFileNameView.getText().toString());
editor.putString(PREFS_FILE_TYPE, mFileTypeView.getText().toString());
editor.putString(PREFS_FILE_SCOPE, mFileScopeView.getText().toString());
editor.putString(PREFS_FILE_SIZE, mFileSizeView.getText().toString());
editor.apply();
showProgressBar();
final boolean keepBond = preferences.getBoolean(SettingsFragment.SETTINGS_KEEP_BOND, false);
final boolean forceDfu = preferences.getBoolean(SettingsFragment.SETTINGS_ASSUME_DFU_NODE, false);
final boolean enablePRNs = preferences.getBoolean(SettingsFragment.SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, Build.VERSION.SDK_INT < Build.VERSION_CODES.M);
String value = preferences.getString(SettingsFragment.SETTINGS_NUMBER_OF_PACKETS, String.valueOf(DfuServiceInitiator.DEFAULT_PRN_VALUE));
int numberOfPackets;
try {
numberOfPackets = Integer.parseInt(value);
} catch (final NumberFormatException e) {
numberOfPackets = DfuServiceInitiator.DEFAULT_PRN_VALUE;
}
final DfuServiceInitiator starter = new DfuServiceInitiator(mSelectedDevice.getAddress())
.setDeviceName(mSelectedDevice.getName())
.setKeepBond(keepBond)
.setForceDfu(forceDfu)
.setPacketsReceiptNotificationsEnabled(enablePRNs)
.setPacketsReceiptNotificationsValue(numberOfPackets)
.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true);
if (mFileType == DfuService.TYPE_AUTO) {
starter.setZip(mFileStreamUri, mFilePath);
if (mScope != null)
starter.setScope(mScope);
} else {
starter.setBinOrHex(mFileType, mFileStreamUri, mFilePath).setInitFile(mInitFileStreamUri, mInitFilePath);
}
starter.start(this, DfuService.class);
}
private void showUploadCancelDialog() {
final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION);
pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_PAUSE);
manager.sendBroadcast(pauseAction);
final UploadCancelFragment fragment = UploadCancelFragment.getInstance();
fragment.show(getSupportFragmentManager(), TAG);
}
/**
* Callback of CONNECT/DISCONNECT button on DfuActivity
*/
public void onConnectClicked(final View view) {
if (isBLEEnabled()) {
showDeviceScanningDialog();
} else {
showBLEDialog();
}
}
@Override
public void onDeviceSelected(final BluetoothDevice device, final String name) {
mSelectedDevice = device;
mUploadButton.setEnabled(mStatusOk);
mDeviceNameView.setText(name != null ? name : getString(R.string.not_available));
}
@Override
public void onDialogCanceled() {
// do nothing
}
private void showProgressBar() {
mProgressBar.setVisibility(View.VISIBLE);
mTextPercentage.setVisibility(View.VISIBLE);
mTextPercentage.setText(null);
mTextUploading.setText(R.string.dfu_status_uploading);
mTextUploading.setVisibility(View.VISIBLE);
mConnectButton.setEnabled(false);
mSelectFileButton.setEnabled(false);
mUploadButton.setEnabled(true);
mUploadButton.setText(R.string.dfu_action_upload_cancel);
}
private void onTransferCompleted() {
clearUI(true);
showToast(R.string.dfu_success);
}
public void onUploadCanceled() {
clearUI(false);
showToast(R.string.dfu_aborted);
}
@Override
public void onCancelUpload() {
mProgressBar.setIndeterminate(true);
mTextUploading.setText(R.string.dfu_status_aborting);
mTextPercentage.setText(null);
}
private void showErrorMessage(final String message) {
clearUI(false);
showToast("Upload failed: " + message);
}
private void clearUI(final boolean clearDevice) {
mProgressBar.setVisibility(View.INVISIBLE);
mTextPercentage.setVisibility(View.INVISIBLE);
mTextUploading.setVisibility(View.INVISIBLE);
mConnectButton.setEnabled(true);
mSelectFileButton.setEnabled(true);
mUploadButton.setEnabled(false);
mUploadButton.setText(R.string.dfu_action_upload);
if (clearDevice) {
mSelectedDevice = null;
mDeviceNameView.setText(R.string.dfu_default_name);
}
// Application may have lost the right to these files if Activity was closed during upload (grant uri permission). Clear file related values.
mFileNameView.setText(null);
mFileTypeView.setText(null);
mFileScopeView.setText(null);
mFileSizeView.setText(null);
mFileStatusView.setText(R.string.dfu_file_status_no_file);
mFilePath = null;
mFileStreamUri = null;
mInitFilePath = null;
mInitFileStreamUri = null;
mStatusOk = false;
}
private void showToast(final int messageResId) {
Toast.makeText(this, messageResId, Toast.LENGTH_SHORT).show();
}
private void showToast(final String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
private boolean isDfuServiceRunning() {
final ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (DfuService.class.getName().equals(service.service.getClassName())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.dfu;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;
/**
* The activity is started only by a remote connected computer using ADB. It shows a list of DFU-supported devices in range and allows user to select target device. The HEX file will be uploaded to
* selected device using {@link DfuService}.
*/
public class DfuInitiatorActivity extends AppCompatActivity implements ScannerFragment.OnDeviceSelectedListener {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// The activity must be started with a path to the HEX file
final Intent intent = getIntent();
if (!intent.hasExtra(DfuService.EXTRA_FILE_PATH))
finish();
if (savedInstanceState == null) {
final ScannerFragment fragment = ScannerFragment.getInstance(null); // Device that is advertising directly does not have the GENERAL_DISCOVERABLE nor LIMITED_DISCOVERABLE flag set.
fragment.show(getSupportFragmentManager(), null);
}
}
@Override
public void onDeviceSelected(final BluetoothDevice device, final String name) {
final Intent intent = getIntent();
final String overwrittenName = intent.getStringExtra(DfuService.EXTRA_DEVICE_NAME);
final String path = intent.getStringExtra(DfuService.EXTRA_FILE_PATH);
final String initPath = intent.getStringExtra(DfuService.EXTRA_INIT_FILE_PATH);
final String address = device.getAddress();
final String finalName = overwrittenName == null ? (name != null ? name : getString(R.string.not_available)) : overwrittenName;
final int type = intent.getIntExtra(DfuService.EXTRA_FILE_TYPE, DfuService.TYPE_AUTO);
final boolean keepBond = intent.getBooleanExtra(DfuService.EXTRA_KEEP_BOND, false);
// Start DFU service with data provided in the intent
final Intent service = new Intent(this, DfuService.class);
service.putExtra(DfuService.EXTRA_DEVICE_ADDRESS, address);
service.putExtra(DfuService.EXTRA_DEVICE_NAME, finalName);
service.putExtra(DfuService.EXTRA_FILE_TYPE, type);
service.putExtra(DfuService.EXTRA_FILE_PATH, path);
if (intent.hasExtra(DfuService.EXTRA_INIT_FILE_PATH))
service.putExtra(DfuService.EXTRA_INIT_FILE_PATH, initPath);
service.putExtra(DfuService.EXTRA_KEEP_BOND, keepBond);
service.putExtra(DfuService.EXTRA_UNSAFE_EXPERIMENTAL_BUTTONLESS_DFU, true);
startService(service);
finish();
}
@Override
public void onDialogCanceled() {
finish();
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.dfu;
import android.app.Activity;
import no.nordicsemi.android.dfu.DfuBaseService;
public class DfuService extends DfuBaseService {
@Override
protected Class<? extends Activity> getNotificationTarget() {
/*
* As a target activity the NotificationActivity is returned, not the MainActivity. This is because the notification must create a new task:
*
* intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
*
* when user press it. Using NotificationActivity we can check whether the new activity is a root activity (that means no other activity was open before)
* or that there is other activity already open. In the later case the notificationActivity will just be closed. System will restore the previous activity.
* However if the application has been closed during upload and user click the notification a NotificationActivity will be launched as a root activity.
* It will create and start the main activity and terminate itself.
*
* This method may be used to restore the target activity in case the application was closed or is open. It may also be used to recreate an activity
* history (see NotificationActivity).
*/
return NotificationActivity.class;
}
@Override
protected boolean isDebug() {
// return BuildConfig.DEBUG;
return true;
}
}

View File

@@ -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.dfu;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import no.nordicsemi.android.nrftoolbox.FeaturesActivity;
public class NotificationActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// If this activity is the root activity of the task, the app is not running
if (isTaskRoot()) {
// Start the app before finishing
final Intent parentIntent = new Intent(this, FeaturesActivity.class);
parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final Intent startAppIntent = new Intent(this, DfuActivity.class);
startAppIntent.putExtras(getIntent().getExtras());
startActivities(new Intent[] { parentIntent, startAppIntent });
}
// Now finish, which will drop the user in to the activity that was at the top
// of the task stack
finish();
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.dfu.adapter;
import android.content.Context;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import no.nordicsemi.android.nrftoolbox.R;
/**
* This adapter displays some file browser applications that can be used to select HEX file. It is used when there is no such app already installed on the device. The hardcoded apps and Google Play
* URLs are specified in res/values/strings_dfu.xml.
*/
public class FileBrowserAppsAdapter extends BaseAdapter {
private final LayoutInflater mInflater;
private final Resources mResources;
public FileBrowserAppsAdapter(final Context context) {
mInflater = LayoutInflater.from(context);
mResources = context.getResources();
}
@Override
public int getCount() {
return mResources.getStringArray(R.array.dfu_app_file_browser).length;
}
@Override
public Object getItem(int position) {
return mResources.getStringArray(R.array.dfu_app_file_browser_action)[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.app_file_browser_item, parent, false);
}
final TextView item = (TextView) view;
item.setText(mResources.getStringArray(R.array.dfu_app_file_browser)[position]);
item.getCompoundDrawablesRelative()[0].setLevel(position);
return view;
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.dfu.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.appcompat.app.AlertDialog;
import android.util.Log;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.dfu.DfuService;
/**
* When cancel button is pressed during uploading this fragment shows uploading cancel dialog
*/
public class UploadCancelFragment extends DialogFragment {
private static final String TAG = "UploadCancelFragment";
private CancelFragmentListener mListener;
public interface CancelFragmentListener {
void onCancelUpload();
}
public static UploadCancelFragment getInstance() {
return new UploadCancelFragment();
}
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
try {
mListener = (CancelFragmentListener) activity;
} catch (final ClassCastException e) {
Log.d(TAG, "The parent Activity must implement CancelFragmentListener interface");
}
}
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity()).setTitle(R.string.dfu_confirmation_dialog_title).setMessage(R.string.dfu_upload_dialog_cancel_message).setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, whichButton) -> {
final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getActivity());
final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION);
pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_ABORT);
manager.sendBroadcast(pauseAction);
mListener.onCancelUpload();
}).setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()).create();
}
@Override
public void onCancel(final DialogInterface dialog) {
final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getActivity());
final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION);
pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_RESUME);
manager.sendBroadcast(pauseAction);
}
}

View File

@@ -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.dfu.fragment;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import no.nordicsemi.android.nrftoolbox.R;
public class ZipInfoFragment extends DialogFragment {
@Override
@NonNull
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_zip_info, null);
return new AlertDialog.Builder(getActivity()).setView(view).setTitle(R.string.dfu_file_info).setPositiveButton(R.string.ok, null).create();
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.dfu.settings;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.AttributeSet;
import android.widget.Toast;
import androidx.preference.Preference;
import no.nordicsemi.android.nrftoolbox.R;
public class AboutDfuPreference extends Preference {
public AboutDfuPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AboutDfuPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onClick() {
final Context context = getContext();
final Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("https://www.nordicsemi.com/DocLib/Content/SDK_Doc/nRF5_SDK/v15-3-0/ble_sdk_app_dfu_bootloader"));
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// is browser installed?
if (intent.resolveActivity(context.getPackageManager()) != null)
context.startActivity(intent);
else {
Toast.makeText(getContext(), R.string.no_application, Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.dfu.settings;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Display the fragment as the main content.
getSupportFragmentManager().beginTransaction().replace(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);
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.dfu.settings;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import android.text.TextUtils;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import no.nordicsemi.android.dfu.DfuServiceInitiator;
import no.nordicsemi.android.dfu.DfuSettingsConstants;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsFragment extends PreferenceFragmentCompat implements DfuSettingsConstants, SharedPreferences.OnSharedPreferenceChangeListener {
public static final String SETTINGS_KEEP_BOND = "settings_keep_bond";
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.settings_dfu);
// set initial values
updateNumberOfPacketsSummary();
updateMBRSize();
}
@Override
public void onResume() {
super.onResume();
// attach the preference change listener. It will update the summary below interval preference
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
super.onPause();
// unregister listener
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
final SharedPreferences preferences = getPreferenceManager().getSharedPreferences();
if (SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED.equals(key)) {
final boolean disabled = !preferences.getBoolean(SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, true);
if (disabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
new AlertDialog.Builder(requireContext()).setMessage(R.string.dfu_settings_dfu_number_of_packets_info).setTitle(R.string.dfu_settings_dfu_information)
.setPositiveButton(R.string.ok, null).show();
}
} else if (SETTINGS_NUMBER_OF_PACKETS.equals(key)) {
updateNumberOfPacketsSummary();
} else if (SETTINGS_MBR_SIZE.equals(key)) {
updateMBRSize();
} else if (SETTINGS_ASSUME_DFU_NODE.equals(key) && sharedPreferences.getBoolean(key, false)) {
new AlertDialog.Builder(requireContext()).setMessage(R.string.dfu_settings_dfu_assume_dfu_mode_info).setTitle(R.string.dfu_settings_dfu_information)
.setPositiveButton(R.string.ok, null)
.show();
}
}
private void updateNumberOfPacketsSummary() {
final PreferenceScreen screen = getPreferenceScreen();
final SharedPreferences preferences = getPreferenceManager().getSharedPreferences();
String value = preferences.getString(SETTINGS_NUMBER_OF_PACKETS, String.valueOf(SETTINGS_NUMBER_OF_PACKETS_DEFAULT));
// Security check
if (TextUtils.isEmpty(value)) {
value = String.valueOf(SETTINGS_NUMBER_OF_PACKETS_DEFAULT);
preferences.edit().putString(SETTINGS_NUMBER_OF_PACKETS, value).apply();
}
screen.findPreference(SETTINGS_NUMBER_OF_PACKETS).setSummary(value);
final int valueInt = Integer.parseInt(value);
if (valueInt > 200 && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
new AlertDialog.Builder(requireContext()).setMessage(R.string.dfu_settings_dfu_number_of_packets_info).setTitle(R.string.dfu_settings_dfu_information)
.setPositiveButton(R.string.ok, null)
.show();
}
}
private void updateMBRSize() {
final PreferenceScreen screen = getPreferenceScreen();
final SharedPreferences preferences = getPreferenceManager().getSharedPreferences();
final String value = preferences.getString(SETTINGS_MBR_SIZE, String.valueOf(DfuServiceInitiator.DEFAULT_MBR_SIZE));
screen.findPreference(SETTINGS_MBR_SIZE).setSummary(value);
}
}

View File

@@ -0,0 +1,263 @@
/*
* 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.gls;
import android.content.Context;
import android.content.res.Resources;
import android.util.Pair;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
import no.nordicsemi.android.nrftoolbox.R;
public class ExpandableRecordAdapter extends BaseExpandableListAdapter {;
private final GlucoseManager mGlucoseManager;
private final LayoutInflater mInflater;
private final Context mContext;
private SparseArray<GlucoseRecord> mRecords;
public ExpandableRecordAdapter(final Context context, final GlucoseManager manager) {
mGlucoseManager = manager;
mContext = context;
mInflater = LayoutInflater.from(context);
mRecords = manager.getRecords().clone();
}
@Override
public void notifyDataSetChanged() {
mRecords = mGlucoseManager.getRecords().clone();
super.notifyDataSetChanged();
}
@Override
public int getGroupCount() {
return mRecords.size();
}
@Override
public Object getGroup(final int groupPosition) {
return mRecords.valueAt(groupPosition);
}
@Override
public long getGroupId(final int groupPosition) {
return mRecords.keyAt(groupPosition);
}
@Override
public View getGroupView(final int position, boolean isExpanded, final View convertView, final ViewGroup parent) {
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.activity_feature_gls_item, parent, false);
final GroupViewHolder holder = new GroupViewHolder();
holder.time = view.findViewById(R.id.time);
holder.details = view.findViewById(R.id.details);
holder.concentration = view.findViewById(R.id.gls_concentration);
view.setTag(holder);
}
final GlucoseRecord record = (GlucoseRecord) getGroup(position);
if (record == null)
return view; // this may happen during closing the activity
final GroupViewHolder holder = (GroupViewHolder) view.getTag();
holder.time.setText(mContext.getString(R.string.gls_timestamp, record.time));
try {
holder.details.setText(mContext.getResources().getStringArray(R.array.gls_type)[record.type]);
} catch (final ArrayIndexOutOfBoundsException e) {
holder.details.setText(mContext.getResources().getStringArray(R.array.gls_type)[0]);
}
if (record.unit == GlucoseRecord.UNIT_kgpl) {
holder.concentration.setText(mContext.getString(R.string.gls_value, record.glucoseConcentration * 100000.0f));
} else {
holder.concentration.setText(mContext.getString(R.string.gls_value, record.glucoseConcentration * 1000.0f));
}
return view;
}
@Override
public int getChildrenCount(final int groupPosition) {
final GlucoseRecord record = (GlucoseRecord) getGroup(groupPosition);
int count = 1 + (record.status != 0 ? 1 : 0); // Sample Location and optional Sensor Status Annunciation
if (record.context != null) {
final GlucoseRecord.MeasurementContext context = record.context;
if (context.carbohydrateId != 0)
count += 1; // Carbohydrate ID and units
if (context.meal != 0)
count += 1; // Meal
if (context.tester != 0)
count += 1; // Tester
if (context.health != 0)
count += 1; // Health
if (context.exerciseDuration != 0)
count += 1; // Duration and intensity
if (context.medicationId != 0)
count += 1; // Medication ID and quantity (with unit)
if (context.HbA1c != 0)
count += 1; // HbA1c
}
return count;
}
@Override
public Object getChild(final int groupPosition, final int childPosition) {
final Resources resources = mContext.getResources();
final GlucoseRecord record = (GlucoseRecord) getGroup(groupPosition);
String tmp;
switch (childIdToItemId(childPosition, record)) {
case 0:
try {
tmp = resources.getStringArray(R.array.gls_location)[record.sampleLocation];
} catch (final ArrayIndexOutOfBoundsException e) {
tmp = resources.getStringArray(R.array.gls_location)[0];
}
return new Pair<>(resources.getString(R.string.gls_location_title), tmp);
case 1: { // sensor status annunciation
final StringBuilder builder = new StringBuilder();
final int status = record.status;
for (int i = 0; i < 12; ++i)
if ((status & (1 << i)) > 0)
builder.append(resources.getStringArray(R.array.gls_status_annunciation)[i]).append("\n");
builder.setLength(builder.length() - 1);
return new Pair<>(resources.getString(R.string.gls_status_annunciation_title), builder.toString());
}
case 2: { // carbohydrate id and unit
try {
tmp = resources.getStringArray(R.array.gls_context_carbohydrare)[record.context.carbohydrateId];
} catch (final ArrayIndexOutOfBoundsException e) {
tmp = resources.getStringArray(R.array.gls_context_carbohydrare)[0];
}
return new Pair<>(resources.getString(R.string.gls_context_carbohydrare_title), tmp + " (" + record.context.carbohydrateUnits + " g)");
}
case 3: { // meal
try {
tmp = resources.getStringArray(R.array.gls_context_meal)[record.context.meal];
} catch (final ArrayIndexOutOfBoundsException e) {
tmp = resources.getStringArray(R.array.gls_context_meal)[0];
}
return new Pair<>(resources.getString(R.string.gls_context_meal_title), tmp);
}
case 4: { // tester
try {
tmp = resources.getStringArray(R.array.gls_context_tester)[record.context.tester];
} catch (final ArrayIndexOutOfBoundsException e) {
tmp = resources.getStringArray(R.array.gls_context_tester)[0];
}
return new Pair<>(resources.getString(R.string.gls_context_tester_title), tmp);
}
case 5: { // health
try {
tmp = resources.getStringArray(R.array.gls_context_health)[record.context.health];
} catch (final ArrayIndexOutOfBoundsException e) {
tmp = resources.getStringArray(R.array.gls_context_health)[0];
}
return new Pair<>(resources.getString(R.string.gls_context_health_title), tmp);
}
case 6: { // exercise duration and intensity
return new Pair<>(resources.getString(R.string.gls_context_exercise_title), resources.getString(R.string.gls_context_exercise, record.context.exerciseDuration, record.context.exerciseIntensity));
}
case 7: { // medication ID and quantity
try {
tmp = resources.getStringArray(R.array.gls_context_medication_id)[record.context.medicationId];
} catch (final ArrayIndexOutOfBoundsException e) {
tmp = resources.getStringArray(R.array.gls_context_medication_id)[0];
}
final int resId = record.context.medicationUnit == GlucoseRecord.UNIT_kgpl ? R.string.gls_context_medication_kg : R.string.gls_context_medication_l;
return new Pair<>(resources.getString(R.string.gls_context_medication_title), resources.getString(resId, tmp, record.context.medicationQuantity));
}
case 8: { // HbA1c value
return new Pair<>(resources.getString(R.string.gls_context_hba1c_title), resources.getString(R.string.gls_context_hba1c, record.context.HbA1c));
}
default:
return new Pair<>("Not implemented", "The value exists but is not shown");
}
}
private int childIdToItemId(final int childPosition, final GlucoseRecord record) {
int itemId = 0;
int child = childPosition;
// Location is required
if (itemId == childPosition)
return itemId;
if (++itemId > 0 && record.status != 0 && --child == 0) return itemId;
if (record.context != null) {
if (++itemId > 0 && record.context.carbohydrateId != 0 && --child == 0) return itemId;
if (++itemId > 0 && record.context.meal != 0 && --child == 0) return itemId;
if (++itemId > 0 && record.context.tester != 0 && --child == 0) return itemId;
if (++itemId > 0 && record.context.health != 0 && --child == 0) return itemId;
if (++itemId > 0 && record.context.exerciseDuration != 0 && --child == 0) return itemId;
if (++itemId > 0 && record.context.medicationId != 0 && --child == 0) return itemId;
if (++itemId > 0 && record.context.HbA1c != 0 && --child == 0) return itemId;
}
throw new IllegalArgumentException("No item ID for position " + childPosition);
}
@Override
public long getChildId(final int groupPosition, final int childPosition) {
return groupPosition + childPosition;
}
@SuppressWarnings("unchecked")
@Override
public View getChildView(final int groupPosition, final int childPosition, final boolean isLastChild, final View convertView, final ViewGroup parent) {
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.activity_feature_gls_subitem, parent, false);
final ChildViewHolder holder = new ChildViewHolder();
holder.title = view.findViewById(android.R.id.text1);
holder.details = view.findViewById(android.R.id.text2);
view.setTag(holder);
}
final Pair<String, String> value = (Pair<String, String>) getChild(groupPosition, childPosition);
final ChildViewHolder holder = (ChildViewHolder) view.getTag();
holder.title.setText(value.first);
holder.details.setText(value.second);
return view;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean isChildSelectable(final int groupPosition, final int childPosition) {
return false;
}
private class GroupViewHolder {
private TextView time;
private TextView details;
private TextView concentration;
}
private class ChildViewHolder {
private TextView title;
private TextView details;
}
}

View File

@@ -0,0 +1,204 @@
/*
* 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.gls;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.util.SparseArray;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.BaseExpandableListAdapter;
import android.widget.PopupMenu;
import android.widget.TextView;
import java.util.UUID;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileExpandableListActivity;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
// TODO The GlucoseActivity should be rewritten to use the service approach, like other do.
public class GlucoseActivity extends BleProfileExpandableListActivity implements PopupMenu.OnMenuItemClickListener, GlucoseManagerCallbacks {
@SuppressWarnings("unused")
private static final String TAG = "GlucoseActivity";
private BaseExpandableListAdapter mAdapter;
private GlucoseManager mGlucoseManager;
private View mControlPanelStd;
private View mControlPanelAbort;
private TextView mUnitView;
private TextView mBatteryLevelView;
@Override
protected void onCreateView(final Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_gls);
setGUI();
}
private void setGUI() {
mUnitView = findViewById(R.id.unit);
mControlPanelStd = findViewById(R.id.gls_control_std);
mControlPanelAbort = findViewById(R.id.gls_control_abort);
mBatteryLevelView = findViewById(R.id.battery);
findViewById(R.id.action_last).setOnClickListener(v -> mGlucoseManager.getLastRecord());
findViewById(R.id.action_all).setOnClickListener(v -> mGlucoseManager.getAllRecords());
findViewById(R.id.action_abort).setOnClickListener(v -> mGlucoseManager.abort());
// create popup menu attached to the button More
findViewById(R.id.action_more).setOnClickListener(v -> {
PopupMenu menu = new PopupMenu(GlucoseActivity.this, v);
menu.setOnMenuItemClickListener(GlucoseActivity.this);
MenuInflater inflater = menu.getMenuInflater();
inflater.inflate(R.menu.gls_more, menu.getMenu());
menu.show();
});
setListAdapter(mAdapter = new ExpandableRecordAdapter(this, mGlucoseManager));
}
@Override
protected LoggableBleManager<GlucoseManagerCallbacks> initializeManager() {
GlucoseManager manager = mGlucoseManager = GlucoseManager.getGlucoseManager(getApplicationContext());
manager.setGattCallbacks(this);
return manager;
}
@Override
public boolean onMenuItemClick(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_refresh:
mGlucoseManager.refreshRecords();
break;
case R.id.action_first:
mGlucoseManager.getFirstRecord();
break;
case R.id.action_clear:
mGlucoseManager.clear();
break;
case R.id.action_delete_all:
mGlucoseManager.deleteAllRecords();
break;
}
return true;
}
@Override
protected int getLoggerProfileTitle() {
return R.string.gls_feature_title;
}
@Override
protected int getAboutTextId() {
return R.string.gls_about_text;
}
@Override
protected int getDefaultDeviceName() {
return R.string.gls_default_name;
}
@Override
protected UUID getFilterUUID() {
return GlucoseManager.GLS_SERVICE_UUID;
}
@Override
protected void setDefaultUI() {
mGlucoseManager.clear();
mBatteryLevelView.setText(R.string.not_available);
}
private void setOperationInProgress(final boolean progress) {
runOnUiThread(() -> {
mControlPanelStd.setVisibility(!progress ? View.VISIBLE : View.GONE);
mControlPanelAbort.setVisibility(progress ? View.VISIBLE : View.GONE);
});
}
@Override
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
super.onDeviceDisconnected(device);
setOperationInProgress(false);
runOnUiThread(() -> mBatteryLevelView.setText(R.string.not_available));
}
@Override
public void onOperationStarted(final BluetoothDevice device) {
setOperationInProgress(true);
}
@Override
public void onOperationCompleted(final BluetoothDevice device) {
setOperationInProgress(false);
runOnUiThread(() -> {
final SparseArray<GlucoseRecord> records = mGlucoseManager.getRecords();
if (records.size() > 0) {
final int unit = records.valueAt(0).unit;
mUnitView.setVisibility(View.VISIBLE);
mUnitView.setText(unit == GlucoseRecord.UNIT_kgpl ? R.string.gls_unit_mgpdl : R.string.gls_unit_mmolpl);
} else {
mUnitView.setVisibility(View.GONE);
}
mAdapter.notifyDataSetChanged();
});
}
@Override
public void onOperationAborted(final BluetoothDevice device) {
setOperationInProgress(false);
}
@Override
public void onOperationNotSupported(final BluetoothDevice device) {
setOperationInProgress(false);
showToast(R.string.gls_operation_not_supported);
}
@Override
public void onOperationFailed(final BluetoothDevice device) {
setOperationInProgress(false);
showToast(R.string.gls_operation_failed);
}
@Override
public void onDatasetChanged(final BluetoothDevice device) {
// Do nothing. Refreshing the list is done in onOperationCompleted
}
@Override
public void onNumberOfRecordsRequested(final BluetoothDevice device, final int value) {
if (value == 0)
showToast(R.string.gls_progress_zero);
else
showToast(getResources().getQuantityString(R.plurals.gls_progress, value, value));
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
runOnUiThread(() -> mBatteryLevelView.setText(getString(R.string.battery, batteryLevel)));
}
}

View File

@@ -0,0 +1,407 @@
/*
* 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.gls;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import android.util.SparseArray;
import java.util.Calendar;
import java.util.UUID;
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback;
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback;
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback;
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.GlucoseMeasurementContextParser;
import no.nordicsemi.android.nrftoolbox.parser.GlucoseMeasurementParser;
import no.nordicsemi.android.nrftoolbox.parser.RecordAccessControlPointParser;
import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
@SuppressWarnings("unused")
public class GlucoseManager extends BatteryManager<GlucoseManagerCallbacks> {
private static final String TAG = "GlucoseManager";
/** Glucose service UUID */
public final static UUID GLS_SERVICE_UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb");
/** Glucose Measurement characteristic UUID */
private final static UUID GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb");
/** Glucose Measurement Context characteristic UUID */
private final static UUID GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb");
/** Glucose Feature characteristic UUID */
private final static UUID GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb");
/** Record Access Control Point characteristic UUID */
private final static UUID RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mGlucoseMeasurementCharacteristic;
private BluetoothGattCharacteristic mGlucoseMeasurementContextCharacteristic;
private BluetoothGattCharacteristic mRecordAccessControlPointCharacteristic;
private final SparseArray<GlucoseRecord> mRecords = new SparseArray<>();
private Handler mHandler;
private static GlucoseManager mInstance;
/**
* Returns the singleton implementation of GlucoseManager.
*/
public static GlucoseManager getGlucoseManager(final Context context) {
if (mInstance == null)
mInstance = new GlucoseManager(context);
return mInstance;
}
private GlucoseManager(final Context context) {
super(context);
mHandler = new Handler();
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving notification, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
super.initialize();
// The gatt.setCharacteristicNotification(...) method is called in BleManager during
// enabling notifications or indications
// (see BleManager#internalEnableNotifications/Indications).
// However, on Samsung S3 with Android 4.3 it looks like the 2 gatt calls
// (gatt.setCharacteristicNotification(...) and gatt.writeDescriptor(...)) are called
// too quickly, or from a wrong thread, and in result the notification listener is not
// set, causing onCharacteristicChanged(...) callback never being called when a
// notification comes. Enabling them here, like below, solves the problem.
// However... the original approach works for the Battery Level CCCD, which makes it
// even weirder.
/*
gatt.setCharacteristicNotification(mGlucoseMeasurementCharacteristic, true);
if (mGlucoseMeasurementContextCharacteristic != null) {
device.setCharacteristicNotification(mGlucoseMeasurementContextCharacteristic, true);
}
device.setCharacteristicNotification(mRecordAccessControlPointCharacteristic, true);
*/
setNotificationCallback(mGlucoseMeasurementCharacteristic)
.with(new GlucoseMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + GlucoseMeasurementParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onGlucoseMeasurementReceived(@NonNull final BluetoothDevice device, final int sequenceNumber,
@NonNull final Calendar time, @Nullable final Float glucoseConcentration,
@Nullable final Integer unit, @Nullable final Integer type,
@Nullable final Integer sampleLocation, @Nullable final GlucoseStatus status,
final boolean contextInformationFollows) {
final GlucoseRecord record = new GlucoseRecord();
record.sequenceNumber = sequenceNumber;
record.time = time;
record.glucoseConcentration = glucoseConcentration != null ? glucoseConcentration : 0;
record.unit = unit != null ? unit : UNIT_kg_L;
record.type = type != null ? type : 0;
record.sampleLocation = sampleLocation != null ? sampleLocation : 0;
record.status = status != null ? status.value : 0;
// insert the new record to storage
mRecords.put(record.sequenceNumber, record);
mHandler.post(() -> {
// if there is no context information following the measurement data,
// notify callback about the new record
if (!contextInformationFollows)
mCallbacks.onDatasetChanged(device);
});
}
});
setNotificationCallback(mGlucoseMeasurementContextCharacteristic)
.with(new GlucoseMeasurementContextDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + GlucoseMeasurementContextParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onGlucoseMeasurementContextReceived(@NonNull final BluetoothDevice device, final int sequenceNumber,
@Nullable final Carbohydrate carbohydrate, @Nullable final Float carbohydrateAmount,
@Nullable final Meal meal, @Nullable final Tester tester,
@Nullable final Health health, @Nullable final Integer exerciseDuration,
@Nullable final Integer exerciseIntensity, @Nullable final Medication medication,
@Nullable final Float medicationAmount, @Nullable final Integer medicationUnit,
@Nullable final Float HbA1c) {
final GlucoseRecord record = mRecords.get(sequenceNumber);
if (record == null) {
DebugLogger.w(TAG, "Context information with unknown sequence number: " + sequenceNumber);
return;
}
final GlucoseRecord.MeasurementContext context = new GlucoseRecord.MeasurementContext();
record.context = context;
context.carbohydrateId = carbohydrate != null ? carbohydrate.value : 0;
context.carbohydrateUnits = carbohydrateAmount != null ? carbohydrateAmount : 0;
context.meal = meal != null ? meal.value : 0;
context.tester = tester != null ? tester.value : 0;
context.health = health != null ? health.value : 0;
context.exerciseDuration = exerciseDuration != null ? exerciseDuration : 0;
context.exerciseIntensity = exerciseIntensity != null ? exerciseIntensity : 0;
context.medicationId = medication != null ? medication.value : 0;
context.medicationQuantity = medicationAmount != null ? medicationAmount : 0;
context.medicationUnit = medicationUnit != null ? medicationUnit : UNIT_mg;
context.HbA1c = HbA1c != null ? HbA1c : 0;
mHandler.post(() -> {
// notify callback about the new record
mCallbacks.onDatasetChanged(device);
});
}
});
setIndicationCallback(mRecordAccessControlPointCharacteristic)
.with(new RecordAccessControlPointDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onRecordAccessOperationCompleted(@NonNull final BluetoothDevice device, final int requestCode) {
switch (requestCode) {
case RACP_OP_CODE_ABORT_OPERATION:
mCallbacks.onOperationAborted(device);
break;
default:
mCallbacks.onOperationCompleted(device);
break;
}
}
@Override
public void onRecordAccessOperationCompletedWithNoRecordsFound(@NonNull final BluetoothDevice device, final int requestCode) {
mCallbacks.onOperationCompleted(device);
}
@Override
public void onNumberOfRecordsReceived(@NonNull final BluetoothDevice device, final int numberOfRecords) {
mCallbacks.onNumberOfRecordsRequested(device, numberOfRecords);
if (numberOfRecords > 0) {
if (mRecords.size() > 0) {
final int sequenceNumber = mRecords.keyAt(mRecords.size() - 1) + 1;
writeCharacteristic(mRecordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber))
.enqueue();
} else {
writeCharacteristic(mRecordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportAllStoredRecords())
.enqueue();
}
} else {
mCallbacks.onOperationCompleted(device);
}
}
@Override
public void onRecordAccessOperationError(@NonNull final BluetoothDevice device,
final int requestCode, final int errorCode) {
log(Log.WARN, "Record Access operation failed (error " + errorCode + ")");
if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
mCallbacks.onOperationNotSupported(device);
} else {
mCallbacks.onOperationFailed(device);
}
}
});
enableNotifications(mGlucoseMeasurementCharacteristic).enqueue();
enableNotifications(mGlucoseMeasurementContextCharacteristic).enqueue();
enableIndications(mRecordAccessControlPointCharacteristic)
.fail((device, status) -> log(Log.WARN, "Failed to enabled Record Access Control Point indications (error " + status + ")"))
.enqueue();
}
@Override
public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(GLS_SERVICE_UUID);
if (service != null) {
mGlucoseMeasurementCharacteristic = service.getCharacteristic(GM_CHARACTERISTIC);
mGlucoseMeasurementContextCharacteristic = service.getCharacteristic(GM_CONTEXT_CHARACTERISTIC);
mRecordAccessControlPointCharacteristic = service.getCharacteristic(RACP_CHARACTERISTIC);
}
return mGlucoseMeasurementCharacteristic != null && mRecordAccessControlPointCharacteristic != null;
}
@Override
protected boolean isOptionalServiceSupported(@NonNull BluetoothGatt gatt) {
super.isOptionalServiceSupported(gatt);
return mGlucoseMeasurementContextCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
mGlucoseMeasurementCharacteristic = null;
mGlucoseMeasurementContextCharacteristic = null;
mRecordAccessControlPointCharacteristic = null;
}
};
/**
* Returns all records as a sparse array where sequence number is the key.
*
* @return the records list.
*/
public SparseArray<GlucoseRecord> getRecords() {
return mRecords;
}
/**
* Clears the records list locally.
*/
public void clear() {
mRecords.clear();
mCallbacks.onOperationCompleted(getBluetoothDevice());
}
/**
* Sends the request to obtain the last (most recent) record from glucose device. The data will
* be returned to Glucose Measurement characteristic as a notification followed by Record Access
* Control Point indication with status code Success or other in case of error.
*/
public void getLastRecord() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.reportLastStoredRecord())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends the request to obtain the first (oldest) record from glucose device. The data will be
* returned to Glucose Measurement characteristic as a notification followed by Record Access
* Control Point indication with status code Success or other in case of error.
*/
public void getFirstRecord() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.reportFirstStoredRecord())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends the request to obtain all records from glucose device. Initially we want to notify user
* about the number of the records so the 'Report Number of Stored Records' is send. The data
* will be returned to Glucose Measurement characteristic as a notification followed by
* Record Access Control Point indication with status code Success or other in case of error.
*/
public void getAllRecords() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.reportNumberOfAllStoredRecords())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends the request to obtain from the glucose device all records newer than the newest one
* from local storage. The data will be returned to Glucose Measurement characteristic as
* a notification followed by Record Access Control Point indication with status code Success
* or other in case of error.
* <p>
* Refresh button will not download records older than the oldest in the local memory.
* E.g. if you have pressed Last and then Refresh, than it will try to get only newer records.
* However if there are no records, it will download all existing (using {@link #getAllRecords()}).
*/
public void refreshRecords() {
if (mRecordAccessControlPointCharacteristic == null)
return;
if (mRecords.size() == 0) {
getAllRecords();
} else {
mCallbacks.onOperationStarted(getBluetoothDevice());
// obtain the last sequence number
final int sequenceNumber = mRecords.keyAt(mRecords.size() - 1) + 1;
writeCharacteristic(mRecordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber))
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
// Info:
// Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2.
}
}
/**
* Sends abort operation signal to the device.
*/
public void abort() {
if (mRecordAccessControlPointCharacteristic == null)
return;
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.abortOperation())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
/**
* Sends the request to delete all data from the device. A Record Access Control Point
* indication with status code Success (or other in case of error) will be send.
*/
public void deleteAllRecords() {
if (mRecordAccessControlPointCharacteristic == null)
return;
clear();
mCallbacks.onOperationStarted(getBluetoothDevice());
writeCharacteristic(mRecordAccessControlPointCharacteristic, RecordAccessControlPointData.deleteAllStoredRecords())
.with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"))
.enqueue();
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.gls;
import android.bluetooth.BluetoothDevice;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
public interface GlucoseManagerCallbacks extends BatteryManagerCallbacks {
void onOperationStarted(final BluetoothDevice device);
void onOperationCompleted(final BluetoothDevice device);
void onOperationFailed(final BluetoothDevice device);
void onOperationAborted(final BluetoothDevice device);
void onOperationNotSupported(final BluetoothDevice device);
void onDatasetChanged(final BluetoothDevice device);
void onNumberOfRecordsRequested(final BluetoothDevice device, final int value);
}

View File

@@ -0,0 +1,118 @@
/*
* 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.gls;
import java.util.Calendar;
public class GlucoseRecord {
public static final int UNIT_kgpl = 0;
public static final int UNIT_molpl = 1;
/** Record sequence number */
protected int sequenceNumber;
/** The base time of the measurement */
protected Calendar time;
/** Time offset of the record */
protected int timeOffset;
/** The glucose concentration. 0 if not present */
protected float glucoseConcentration;
/** Concentration unit. One of the following: {@link GlucoseRecord#UNIT_kgpl}, {@link GlucoseRecord#UNIT_molpl} */
protected int unit;
/** The type of the record. 0 if not present */
protected int type;
/** The sample location. 0 if unknown */
protected int sampleLocation;
/** Sensor status annunciation flags. 0 if not present */
protected int status;
protected MeasurementContext context;
public static class MeasurementContext {
public static final int UNIT_kg = 0;
public static final int UNIT_l = 1;
/**
* One of the following:<br/>
* 0 Not present<br/>
* 1 Breakfast<br/>
* 2 Lunch<br/>
* 3 Dinner<br/>
* 4 Snack<br/>
* 5 Drink<br/>
* 6 Supper<br/>
* 7 Brunch
*/
protected int carbohydrateId;
/** Number of kilograms of carbohydrate */
protected float carbohydrateUnits;
/**
* One of the following:<br/>
* 0 Not present<br/>
* 1 Preprandial (before meal)<br/>
* 2 Postprandial (after meal)<br/>
* 3 Fasting<br/>
* 4 Casual (snacks, drinks, etc.)<br/>
* 5 Bedtime
*/
protected int meal;
/**
* One of the following:<br/>
* 0 Not present<br/>
* 1 Self<br/>
* 2 Health Care Professional<br/>
* 3 Lab test<br/>
* 15 Tester value not available
*/
protected int tester;
/**
* One of the following:<br/>
* 0 Not present<br/>
* 1 Minor health issues<br/>
* 2 Major health issues<br/>
* 3 During menses<br/>
* 4 Under stress<br/>
* 5 No health issues<br/>
* 15 Tester value not available
*/
protected int health;
/** Exercise duration in seconds. 0 if not present */
protected int exerciseDuration;
/** Exercise intensity in percent. 0 if not present */
protected int exerciseIntensity;
/**
* One of the following:<br/>
* 0 Not present<br/>
* 1 Rapid acting insulin<br/>
* 2 Short acting insulin<br/>
* 3 Intermediate acting insulin<br/>
* 4 Long acting insulin<br/>
* 5 Pre-mixed insulin
*/
protected int medicationId;
/** Quantity of medication. See {@link #medicationUnit} for the unit. */
protected float medicationQuantity;
/** One of the following: {@link GlucoseRecord.MeasurementContext#UNIT_kg}, {@link GlucoseRecord.MeasurementContext#UNIT_l}. */
protected int medicationUnit;
/** HbA1c value. 0 if not present */
protected float HbA1c;
}
}

View File

@@ -0,0 +1,248 @@
/*
* 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.hrs;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.graphics.Point;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.ViewGroup;
import android.widget.TextView;
import org.achartengine.GraphicalView;
import java.util.List;
import java.util.UUID;
import no.nordicsemi.android.nrftoolbox.FeaturesActivity;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileActivity;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
/**
* HRSActivity is the main Heart rate activity. It implements HRSManagerCallbacks to receive callbacks from HRSManager class. The activity supports portrait and landscape orientations. The activity
* uses external library AChartEngine to show real time graph of HR values.
*/
// TODO The HRSActivity should be rewritten to use the service approach, like other do.
public class HRSActivity extends BleProfileActivity implements HRSManagerCallbacks {
@SuppressWarnings("unused")
private final String TAG = "HRSActivity";
private final static String GRAPH_STATUS = "graph_status";
private final static String GRAPH_COUNTER = "graph_counter";
private final static String HR_VALUE = "hr_value";
private final static int REFRESH_INTERVAL = 1000; // 1 second interval
private Handler mHandler = new Handler();
private boolean isGraphInProgress = false;
private GraphicalView mGraphView;
private LineGraphView mLineGraph;
private TextView mHRSValue, mHRSPosition;
private TextView mBatteryLevelView;
private int mHrmValue = 0;
private int mCounter = 0;
@Override
protected void onCreateView(final Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_hrs);
setGUI();
}
private void setGUI() {
mLineGraph = LineGraphView.getLineGraphView();
mHRSValue = findViewById(R.id.text_hrs_value);
mHRSPosition = findViewById(R.id.text_hrs_position);
mBatteryLevelView = findViewById(R.id.battery);
showGraph();
}
private void showGraph() {
mGraphView = mLineGraph.getView(this);
ViewGroup layout = findViewById(R.id.graph_hrs);
layout.addView(mGraphView);
}
@Override
protected void onStart() {
super.onStart();
final Intent intent = getIntent();
if (!isDeviceConnected() && intent.hasExtra(FeaturesActivity.EXTRA_ADDRESS)) {
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(getIntent().getByteArrayExtra(FeaturesActivity.EXTRA_ADDRESS));
onDeviceSelected(device, device.getName());
intent.removeExtra(FeaturesActivity.EXTRA_APP);
intent.removeExtra(FeaturesActivity.EXTRA_ADDRESS);
}
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
isGraphInProgress = savedInstanceState.getBoolean(GRAPH_STATUS);
mCounter = savedInstanceState.getInt(GRAPH_COUNTER);
mHrmValue = savedInstanceState.getInt(HR_VALUE);
if (isGraphInProgress)
startShowGraph();
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(GRAPH_STATUS, isGraphInProgress);
outState.putInt(GRAPH_COUNTER, mCounter);
outState.putInt(HR_VALUE, mHrmValue);
}
@Override
protected void onDestroy() {
super.onDestroy();
stopShowGraph();
}
@Override
protected int getLoggerProfileTitle() {
return R.string.hrs_feature_title;
}
@Override
protected int getAboutTextId() {
return R.string.hrs_about_text;
}
@Override
protected int getDefaultDeviceName() {
return R.string.hrs_default_name;
}
@Override
protected UUID getFilterUUID() {
return HRSManager.HR_SERVICE_UUID;
}
private void updateGraph(final int hrmValue) {
mCounter++;
mLineGraph.addValue(new Point(mCounter, hrmValue));
mGraphView.repaint();
}
private Runnable mRepeatTask = new Runnable() {
@Override
public void run() {
if (mHrmValue > 0)
updateGraph(mHrmValue);
if (isGraphInProgress)
mHandler.postDelayed(mRepeatTask, REFRESH_INTERVAL);
}
};
void startShowGraph() {
isGraphInProgress = true;
mRepeatTask.run();
}
void stopShowGraph() {
isGraphInProgress = false;
mHandler.removeCallbacks(mRepeatTask);
}
@Override
protected LoggableBleManager<HRSManagerCallbacks> initializeManager() {
final HRSManager manager = HRSManager.getInstance(getApplicationContext());
manager.setGattCallbacks(this);
return manager;
}
@Override
public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) {
// this may notify user or show some views
}
@Override
public void onDeviceReady(@NonNull final BluetoothDevice device) {
startShowGraph();
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
runOnUiThread(() -> mBatteryLevelView.setText(getString(R.string.battery, batteryLevel)));
}
@Override
public void onBodySensorLocationReceived(@NonNull final BluetoothDevice device, final int sensorLocation) {
runOnUiThread(() -> {
if (sensorLocation >= SENSOR_LOCATION_FIRST && sensorLocation <= SENSOR_LOCATION_LAST) {
mHRSPosition.setText(getResources().getStringArray(R.array.hrs_locations)[sensorLocation]);
} else {
mHRSPosition.setText(R.string.hrs_location_other);
}
});
}
@Override
public void onHeartRateMeasurementReceived(@NonNull final BluetoothDevice device, final int heartRate,
@Nullable final Boolean contactDetected,
@Nullable final Integer energyExpanded,
@Nullable final List<Integer> rrIntervals) {
mHrmValue = heartRate;
runOnUiThread(() -> mHRSValue.setText(getString(R.string.hrs_value, heartRate)));
}
@Override
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
super.onDeviceDisconnected(device);
runOnUiThread(() -> {
mHRSValue.setText(R.string.not_available_value);
mHRSPosition.setText(R.string.not_available);
mBatteryLevelView.setText(R.string.not_available);
stopShowGraph();
});
}
@Override
protected void setDefaultUI() {
mHRSValue.setText(R.string.not_available_value);
mHRSPosition.setText(R.string.not_available);
mBatteryLevelView.setText(R.string.not_available);
clearGraph();
}
private void clearGraph() {
mLineGraph.clearGraph();
mGraphView.repaint();
mCounter = 0;
mHrmValue = 0;
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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.hrs;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import java.util.List;
import java.util.UUID;
import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallback;
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.BodySensorLocationParser;
import no.nordicsemi.android.nrftoolbox.parser.HeartRateMeasurementParser;
/**
* HRSManager class performs BluetoothGatt operations for connection, service discovery,
* enabling notification and reading characteristics.
* All operations required to connect to device with BLE Heart Rate Service and reading
* heart rate values are performed here.
*/
public class HRSManager extends BatteryManager<HRSManagerCallbacks> {
static final UUID HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb");
private static final UUID BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb");
private static final UUID HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mHeartRateCharacteristic, mBodySensorLocationCharacteristic;
private static HRSManager managerInstance = null;
/**
* Singleton implementation of HRSManager class.
*/
public static synchronized HRSManager getInstance(final Context context) {
if (managerInstance == null) {
managerInstance = new HRSManager(context);
}
return managerInstance;
}
private HRSManager(final Context context) {
super(context);
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving notification, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
super.initialize();
readCharacteristic(mBodySensorLocationCharacteristic)
.with(new BodySensorLocationDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + BodySensorLocationParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onBodySensorLocationReceived(@NonNull final BluetoothDevice device,
final int sensorLocation) {
mCallbacks.onBodySensorLocationReceived(device, sensorLocation);
}
})
.fail((device, status) -> log(Log.WARN, "Body Sensor Location characteristic not found"))
.enqueue();
setNotificationCallback(mHeartRateCharacteristic)
.with(new HeartRateMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + HeartRateMeasurementParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onHeartRateMeasurementReceived(@NonNull final BluetoothDevice device,
final int heartRate,
@Nullable final Boolean contactDetected,
@Nullable final Integer energyExpanded,
@Nullable final List<Integer> rrIntervals) {
mCallbacks.onHeartRateMeasurementReceived(device, heartRate, contactDetected, energyExpanded, rrIntervals);
}
});
enableNotifications(mHeartRateCharacteristic).enqueue();
}
@Override
protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(HR_SERVICE_UUID);
if (service != null) {
mHeartRateCharacteristic = service.getCharacteristic(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID);
}
return mHeartRateCharacteristic != null;
}
@Override
protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
super.isOptionalServiceSupported(gatt);
final BluetoothGattService service = gatt.getService(HR_SERVICE_UUID);
if (service != null) {
mBodySensorLocationCharacteristic = service.getCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID);
}
return mBodySensorLocationCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
super.onDeviceDisconnected();
mBodySensorLocationCharacteristic = null;
mHeartRateCharacteristic = null;
}
};
}

View File

@@ -0,0 +1,30 @@
/*
* 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.hrs;
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocationCallback;
import no.nordicsemi.android.ble.common.profile.hr.HeartRateMeasurementCallback;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
interface HRSManagerCallbacks extends BatteryManagerCallbacks, BodySensorLocationCallback, HeartRateMeasurementCallback {
}

View File

@@ -0,0 +1,121 @@
/*
* 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.hrs;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint.Align;
import android.graphics.Point;
import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.chart.PointStyle;
import org.achartengine.model.TimeSeries;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;
/**
* This class uses external library AChartEngine to show dynamic real time line graph for HR values
*/
public class LineGraphView {
//TimeSeries will hold the data in x,y format for single chart
private TimeSeries mSeries = new TimeSeries("Heart Rate");
//XYMultipleSeriesDataset will contain all the TimeSeries
private XYMultipleSeriesDataset mDataset = new XYMultipleSeriesDataset();
//XYMultipleSeriesRenderer will contain all XYSeriesRenderer and it can be used to set the properties of whole Graph
private XYMultipleSeriesRenderer mMultiRenderer = new XYMultipleSeriesRenderer();
private static LineGraphView mInstance = null;
/**
* singleton implementation of LineGraphView class
*/
public static synchronized LineGraphView getLineGraphView() {
if (mInstance == null) {
mInstance = new LineGraphView();
}
return mInstance;
}
/**
* This constructor will set some properties of single chart and some properties of whole graph
*/
public LineGraphView() {
//add single line chart mSeries
mDataset.addSeries(mSeries);
//XYSeriesRenderer is used to set the properties like chart color, style of each point, etc. of single chart
final XYSeriesRenderer seriesRenderer = new XYSeriesRenderer();
//set line chart color to Black
seriesRenderer.setColor(Color.BLACK);
//set line chart style to square points
seriesRenderer.setPointStyle(PointStyle.SQUARE);
seriesRenderer.setFillPoints(true);
final XYMultipleSeriesRenderer renderer = mMultiRenderer;
//set whole graph background color to transparent color
renderer.setBackgroundColor(Color.TRANSPARENT);
renderer.setMargins(new int[] { 50, 65, 40, 5 }); // top, left, bottom, right
renderer.setMarginsColor(Color.argb(0x00, 0x01, 0x01, 0x01));
renderer.setAxesColor(Color.BLACK);
renderer.setAxisTitleTextSize(24);
renderer.setShowGrid(true);
renderer.setGridColor(Color.LTGRAY);
renderer.setLabelsColor(Color.BLACK);
renderer.setYLabelsColor(0, Color.DKGRAY);
renderer.setYLabelsAlign(Align.RIGHT);
renderer.setYLabelsPadding(4.0f);
renderer.setXLabelsColor(Color.DKGRAY);
renderer.setLabelsTextSize(20);
renderer.setLegendTextSize(20);
//Disable zoom
renderer.setPanEnabled(false, false);
renderer.setZoomEnabled(false, false);
//set title to x-axis and y-axis
renderer.setXTitle(" Time (seconds)");
renderer.setYTitle(" BPM");
renderer.addSeriesRenderer(seriesRenderer);
}
/**
* return graph view to activity
*/
public GraphicalView getView(Context context) {
final GraphicalView graphView = ChartFactory.getLineChartView(context, mDataset, mMultiRenderer);
return graphView;
}
/**
* add new x,y value to chart
*/
public void addValue(Point p) {
mSeries.add(p.x, p.y);
}
/**
* clear all previous values of chart
*/
public void clearGraph() {
mSeries.clear();
}
}

View File

@@ -0,0 +1,223 @@
/*
* 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.hts;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.localbroadcastmanager.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.hts.settings.SettingsActivity;
import no.nordicsemi.android.nrftoolbox.hts.settings.SettingsFragment;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileService;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity;
/**
* HTSActivity is the main Health Thermometer activity. It implements {@link HTSManagerCallbacks}
* to receive callbacks from {@link HTSManager} class. The activity supports portrait and landscape
* orientations.
*/
public class HTSActivity extends BleProfileServiceReadyActivity<HTSService.HTSBinder> {
@SuppressWarnings("unused")
private final String TAG = "HTSActivity";
private TextView mTempValue;
private TextView mUnit;
private TextView mBatteryLevelView;
@Override
protected void onCreateView(final Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_hts);
setGUI();
}
@Override
protected void onInitialize(final Bundle savedInstanceState) {
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, makeIntentFilter());
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
}
private void setGUI() {
mTempValue = findViewById(R.id.text_hts_value);
mUnit = findViewById(R.id.text_hts_unit);
mBatteryLevelView = findViewById(R.id.battery);
}
@Override
protected void onResume() {
super.onResume();
setUnits();
}
@Override
protected void setDefaultUI() {
mTempValue.setText(R.string.not_available_value);
mBatteryLevelView.setText(R.string.not_available);
setUnits();
}
private void setUnits() {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT)));
switch (unit) {
case SettingsFragment.SETTINGS_UNIT_C:
mUnit.setText(R.string.hts_unit_celsius);
break;
case SettingsFragment.SETTINGS_UNIT_F:
mUnit.setText(R.string.hts_unit_fahrenheit);
break;
case SettingsFragment.SETTINGS_UNIT_K:
mUnit.setText(R.string.hts_unit_kelvin);
break;
}
}
@Override
protected void onServiceBound(final HTSService.HTSBinder binder) {
onTemperatureMeasurementReceived(binder.getTemperature());
}
@Override
protected void onServiceUnbound() {
// not used
}
@Override
protected int getLoggerProfileTitle() {
return R.string.hts_feature_title;
}
@Override
protected int getAboutTextId() {
return R.string.hts_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.hts_default_name;
}
@Override
protected UUID getFilterUUID() {
return HTSManager.HT_SERVICE_UUID;
}
@Override
protected Class<? extends BleProfileService> getServiceClass() {
return HTSService.class;
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, boolean optionalServicesFound) {
// this may notify user or show some views
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
super.onDeviceDisconnected(device);
mBatteryLevelView.setText(R.string.not_available);
}
private void onTemperatureMeasurementReceived(Float value) {
if (value != null) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT,
String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT)));
switch (unit) {
case SettingsFragment.SETTINGS_UNIT_F:
value = value * 1.8f + 32f;
break;
case SettingsFragment.SETTINGS_UNIT_K:
value += 273.15f;
break;
case SettingsFragment.SETTINGS_UNIT_C:
break;
}
mTempValue.setText(getString(R.string.hts_value, value));
} else {
mTempValue.setText(R.string.not_available_value);
}
}
public void onBatteryLevelChanged(final int value) {
mBatteryLevelView.setText(getString(R.string.battery, value));
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
if (HTSService.BROADCAST_HTS_MEASUREMENT.equals(action)) {
final float value = intent.getFloatExtra(HTSService.EXTRA_TEMPERATURE, 0.0f);
// Update GUI
onTemperatureMeasurementReceived(value);
} else if (HTSService.BROADCAST_BATTERY_LEVEL.equals(action)) {
final int batteryLevel = intent.getIntExtra(HTSService.EXTRA_BATTERY_LEVEL, 0);
// Update GUI
onBatteryLevelChanged(batteryLevel);
}
}
};
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(HTSService.BROADCAST_HTS_MEASUREMENT);
intentFilter.addAction(HTSService.BROADCAST_BATTERY_LEVEL);
return intentFilter;
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.hts;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Calendar;
import java.util.UUID;
import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.TemperatureMeasurementParser;
/**
* HTSManager class performs BluetoothGatt operations for connection, service discovery, enabling
* indication and reading characteristics. All operations required to connect to device with BLE HT
* Service and reading health thermometer values are performed here.
* HTSActivity implements HTSManagerCallbacks in order to receive callbacks of BluetoothGatt operations.
*/
public class HTSManager extends BatteryManager<HTSManagerCallbacks> {
/** Health Thermometer service UUID */
public final static UUID HT_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb");
/** Health Thermometer Measurement characteristic UUID */
private static final UUID HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mHTCharacteristic;
HTSManager(final Context context) {
super(context);
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc..
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
super.initialize();
setIndicationCallback(mHTCharacteristic)
.with(new TemperatureMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + TemperatureMeasurementParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onTemperatureMeasurementReceived(@NonNull final BluetoothDevice device,
final float temperature, final int unit,
@Nullable final Calendar calendar,
@Nullable final Integer type) {
mCallbacks.onTemperatureMeasurementReceived(device, temperature, unit, calendar, type);
}
});
enableIndications(mHTCharacteristic).enqueue();
}
@Override
protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(HT_SERVICE_UUID);
if (service != null) {
mHTCharacteristic = service.getCharacteristic(HT_MEASUREMENT_CHARACTERISTIC_UUID);
}
return mHTCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
super.onDeviceDisconnected();
mHTCharacteristic = null;
}
};
}

View File

@@ -0,0 +1,33 @@
/*
* 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.hts;
import no.nordicsemi.android.ble.common.profile.ht.TemperatureMeasurementCallback;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
/**
* Interface {@link HTSManagerCallbacks} must be implemented by {@link HTSActivity} in order
* to receive callbacks from {@link HTSManager}.
*/
interface HTSManagerCallbacks extends BatteryManagerCallbacks, TemperatureMeasurementCallback {
}

View File

@@ -0,0 +1,208 @@
/*
* 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.hts;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.Calendar;
import no.nordicsemi.android.ble.common.profile.ht.TemperatureMeasurementCallback;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.FeaturesActivity;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.ToolboxApplication;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileService;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
@SuppressWarnings("FieldCanBeLocal")
public class HTSService extends BleProfileService implements HTSManagerCallbacks {
public static final String BROADCAST_HTS_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.hts.BROADCAST_HTS_MEASUREMENT";
public static final String EXTRA_TEMPERATURE = "no.nordicsemi.android.nrftoolbox.hts.EXTRA_TEMPERATURE";
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.hts.ACTION_DISCONNECT";
private final static int NOTIFICATION_ID = 267;
private final static int OPEN_ACTIVITY_REQ = 0;
private final static int DISCONNECT_REQ = 1;
/** The last received temperature value in Celsius degrees. */
private Float mTemp;
@SuppressWarnings("unused")
private HTSManager mManager;
private final LocalBinder mBinder = new HTSBinder();
/**
* This local binder is an interface for the bonded activity to operate with the HTS sensor
*/
class HTSBinder extends LocalBinder {
/**
* Returns the last received temperature value.
*
* @return Temperature value in Celsius.
*/
Float getTemperature() {
return mTemp;
}
}
@Override
protected LocalBinder getBinder() {
return mBinder;
}
@Override
protected LoggableBleManager<HTSManagerCallbacks> initializeManager() {
return mManager = new HTSManager(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.hts_notification_connected_message, 0);
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
super.onDeviceDisconnected(device);
mTemp = null;
}
@Override
public void onTemperatureMeasurementReceived(@NonNull final BluetoothDevice device,
final float temperature, final int unit,
@Nullable final Calendar calendar,
@Nullable final Integer type) {
mTemp = TemperatureMeasurementCallback.toCelsius(temperature, unit);
final Intent broadcast = new Intent(BROADCAST_HTS_MEASUREMENT);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_TEMPERATURE, mTemp);
// ignore the rest
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
if (!mBound) {
// Here we may update the notification to display the current temperature.
// TODO modify the notification here
}
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
/**
* Creates the notification
*
* @param messageResId
* message resource id. The message must have one String parameter,<br />
* f.e. <code>&lt;string name="name"&gt;%s is connected&lt;/string&gt;</code>
* @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, HTSActivity.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 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL);
builder.setContentIntent(pendingIntent);
builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName()));
builder.setSmallIcon(R.drawable.ic_stat_notify_hts);
builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true);
builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.hts_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();
}
};
}

View File

@@ -0,0 +1,56 @@
/*
* 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.hts.settings;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Display the fragment as the main content.
getSupportFragmentManager().beginTransaction().replace(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);
}
}

View File

@@ -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.hts.settings;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import androidx.preference.PreferenceFragmentCompat;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsFragment extends PreferenceFragmentCompat {
public static final String SETTINGS_UNIT = "settings_hts_unit";
public static final int SETTINGS_UNIT_C = 0; // [C]
public static final int SETTINGS_UNIT_F = 1; // [F]
public static final int SETTINGS_UNIT_K = 2; // [K]
public static final int SETTINGS_UNIT_DEFAULT = SETTINGS_UNIT_C;
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.settings_hts);
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.parser;
import android.bluetooth.BluetoothGattCharacteristic;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class AlertLevelParser {
public static String parse(final BluetoothGattCharacteristic characteristic) {
return parse(Data.from(characteristic));
}
/**
* Parses the alert level.
*
* @param data
* @return alert level in human readable format
*/
public static String parse(final Data data) {
final int value = data.getIntValue(Data.FORMAT_UINT8, 0);
switch (value) {
case 0:
return "No Alert";
case 1:
return "Mild Alert";
case 2:
return "High Alert";
default:
return "Reserved value (" + value + ")";
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class BloodPressureMeasurementParser {
public static String parse(final Data data) {
final StringBuilder builder = new StringBuilder();
// first byte - flags
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
final int unitType = flags & 0x01;
final boolean timestampPresent = (flags & 0x02) > 0;
final boolean pulseRatePresent = (flags & 0x04) > 0;
final boolean userIdPresent = (flags & 0x08) > 0;
final boolean statusPresent = (flags & 0x10) > 0;
// following bytes - systolic, diastolic and mean arterial pressure
final float systolic = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
final float diastolic = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 2);
final float meanArterialPressure = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 4);
final String unit = unitType == 0 ? " mmHg" : " kPa";
offset += 6;
builder.append("Systolic: ").append(systolic).append(unit);
builder.append("\nDiastolic: ").append(diastolic).append(unit);
builder.append("\nMean AP: ").append(meanArterialPressure).append(unit);
// parse timestamp if present
if (timestampPresent) {
builder.append("\nTimestamp: ").append(DateTimeParser.parse(data, offset));
offset += 7;
}
// parse pulse rate if present
if (pulseRatePresent) {
final float pulseRate = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
offset += 2;
builder.append("\nPulse: ").append(pulseRate).append(" bpm");
}
if (userIdPresent) {
final int userId = data.getIntValue(Data.FORMAT_UINT8, offset);
offset += 1;
builder.append("\nUser ID: ").append(userId);
}
if (statusPresent) {
final int status = data.getIntValue(Data.FORMAT_UINT16, offset);
// offset += 2;
if ((status & 0x0001) > 0)
builder.append("\nBody movement detected");
if ((status & 0x0002) > 0)
builder.append("\nCuff too lose");
if ((status & 0x0004) > 0)
builder.append("\nIrregular pulse detected");
if ((status & 0x0018) == 0x0008)
builder.append("\nPulse rate exceeds upper limit");
if ((status & 0x0018) == 0x0010)
builder.append("\nPulse rate is less than lower limit");
if ((status & 0x0018) == 0x0018)
builder.append("\nPulse rate range: Reserved for future use ");
if ((status & 0x0020) > 0)
builder.append("\nImproper measurement position");
}
return builder.toString();
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class BodySensorLocationParser {
public static String parse(final Data data) {
final int value = data.getIntValue(Data.FORMAT_UINT8, 0);
switch (value) {
case 6: return "Foot";
case 5: return "Ear Lobe";
case 4: return "Hand";
case 3: return "Finger";
case 2: return "Wrist";
case 1: return "Chest";
case 0:
default: return "Other";
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* 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.parser;
import java.util.Locale;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class CGMMeasurementParser {
private static final int FLAGS_CGM_TREND_INFO_PRESENT = 1;
private static final int FLAGS_CGM_QUALITY_PRESENT = 1 << 1;
private static final int FLAGS_SENSOR_STATUS_ANNUNCIATION_WARNING_OCTET_PRESENT = 1 << 2;
private static final int FLAGS_SENSOR_STATUS_ANNUNCIATION_CAL_TEMP_OCTET_PRESENT = 1 << 3;
private static final int FLAGS_SENSOR_STATUS_ANNUNCIATION_STATUS_OCTET_PRESENT = 1 << 4;
private static final int SSA_SESSION_STOPPED = 1;
private static final int SSA_DEVICE_BATTERY_LOW = 1 << 1;
private static final int SSA_SENSOR_TYPE_INCORRECT = 1 << 2;
private static final int SSA_SENSOR_MALFUNCTION = 1 << 3;
private static final int SSA_DEVICE_SPEC_ALERT = 1 << 4;
private static final int SSA_GENERAL_DEVICE_FAULT = 1 << 5;
private static final int SSA_TIME_SYNC_REQUIRED = 1 << 8;
private static final int SSA_CALIBRATION_NOT_ALLOWED = 1 << 9;
private static final int SSA_CALIBRATION_RECOMMENDED = 1 << 10;
private static final int SSA_CALIBRATION_REQUIRED = 1 << 11;
private static final int SSA_SENSOR_TEMP_TOO_HIGH = 1 << 12;
private static final int SSA_SENSOR_TEMP_TOO_LOW = 1 << 13;
private static final int SSA_RESULT_LOWER_THAN_PATIENT_LOW_LEVEL = 1 << 16;
private static final int SSA_RESULT_HIGHER_THAN_PATIENT_HIGH_LEVEL = 1 << 17;
private static final int SSA_RESULT_LOWER_THAN_HYPO_LEVEL = 1 << 18;
private static final int SSA_RESULT_HIGHER_THAN_HYPER_LEVEL = 1 << 19;
private static final int SSA_SENSOR_RATE_OF_DECREASE_EXCEEDED = 1 << 20;
private static final int SSA_SENSOR_RATE_OF_INCREASE_EXCEEDED = 1 << 21;
private static final int SSA_RESULT_LOWER_THAN_DEVICE_CAN_PROCESS = 1 << 22;
private static final int SSA_RESULT_HIGHER_THAN_DEVICE_CAN_PROCESS = 1 << 23;
public static String parse(final Data data) {
// The CGM Measurement characteristic is a variable length structure containing one or more CGM Measurement records
int totalSize = data.getValue().length;
final StringBuilder builder = new StringBuilder();
int offset = 0;
while (offset < totalSize) {
offset += parseRecord(builder, data, offset);
if (offset < totalSize)
builder.append("\n\n");
}
return builder.toString();
}
private static int parseRecord(final StringBuilder builder, final Data data, int offset) {
// Read size and flags bytes
final int size = data.getIntValue(Data.FORMAT_UINT8, offset++);
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
/*
* false CGM Trend Information is not preset
* true CGM Trend Information is preset
*/
final boolean cgmTrendInformationPresent = (flags & FLAGS_CGM_TREND_INFO_PRESENT) > 0;
/*
* false CGM Quality is not preset
* true CGM Quality is preset
*/
final boolean cgmQualityPresent = (flags & FLAGS_CGM_QUALITY_PRESENT) > 0;
/*
* false Sensor Status Annunciation - Warning-Octet is not preset
* true Sensor Status Annunciation - Warning-Octet is preset
*/
final boolean ssaWarningOctetPresent = (flags & FLAGS_SENSOR_STATUS_ANNUNCIATION_WARNING_OCTET_PRESENT) > 0;
/*
* false Sensor Status Annunciation - Calibration/Temp-Octet is not preset
* true Sensor Status Annunciation - Calibration/Temp-Octet is preset
*/
final boolean ssaCalTempOctetPresent = (flags & FLAGS_SENSOR_STATUS_ANNUNCIATION_CAL_TEMP_OCTET_PRESENT) > 0;
/*
* false Sensor Status Annunciation - Status-Octet is not preset
* true Sensor Status Annunciation - Status-Octet is preset
*/
final boolean ssaStatusOctetPresent = (flags & FLAGS_SENSOR_STATUS_ANNUNCIATION_STATUS_OCTET_PRESENT) > 0;
// Read CGM Glucose Concentration
final float glucoseConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
offset += 2;
// Read time offset
final int timeOffset = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
builder.append("Glucose concentration: ").append(glucoseConcentration).append(" mg/dL\n");
builder.append("Sequence number: ").append(timeOffset).append(" (Time Offset in min)\n");
if (ssaWarningOctetPresent) {
final int ssaWarningOctet = data.getIntValue(Data.FORMAT_UINT8, offset++);
builder.append("Warnings:\n");
if ((ssaWarningOctet & SSA_SESSION_STOPPED) > 0)
builder.append("- Session Stopped\n");
if ((ssaWarningOctet & SSA_DEVICE_BATTERY_LOW) > 0)
builder.append("- Device Battery Low\n");
if ((ssaWarningOctet & SSA_SENSOR_TYPE_INCORRECT) > 0)
builder.append("- Sensor Type Incorrect\n");
if ((ssaWarningOctet & SSA_SENSOR_MALFUNCTION) > 0)
builder.append("- Sensor Malfunction\n");
if ((ssaWarningOctet & SSA_DEVICE_SPEC_ALERT) > 0)
builder.append("- Device Specific Alert\n");
if ((ssaWarningOctet & SSA_GENERAL_DEVICE_FAULT) > 0)
builder.append("- General Device Fault\n");
}
if (ssaCalTempOctetPresent) {
final int ssaCalTempOctet = data.getIntValue(Data.FORMAT_UINT8, offset++);
builder.append("Cal/Temp Info:\n");
if ((ssaCalTempOctet & SSA_TIME_SYNC_REQUIRED) > 0)
builder.append("- Time Synchronization Required\n");
if ((ssaCalTempOctet & SSA_CALIBRATION_NOT_ALLOWED) > 0)
builder.append("- Calibration Not Allowed\n");
if ((ssaCalTempOctet & SSA_CALIBRATION_RECOMMENDED) > 0)
builder.append("- Calibration Recommended\n");
if ((ssaCalTempOctet & SSA_CALIBRATION_REQUIRED) > 0)
builder.append("- Calibration Required\n");
if ((ssaCalTempOctet & SSA_SENSOR_TEMP_TOO_HIGH) > 0)
builder.append("- Sensor Temp Too High\n");
if ((ssaCalTempOctet & SSA_SENSOR_TEMP_TOO_LOW) > 0)
builder.append("- Sensor Temp Too Low\n");
}
if (ssaStatusOctetPresent) {
final int ssaStatusOctet = data.getIntValue(Data.FORMAT_UINT8, offset++);
builder.append("Status:\n");
if ((ssaStatusOctet & SSA_RESULT_LOWER_THAN_PATIENT_LOW_LEVEL) > 0)
builder.append("- Result Lower then Patient Low Level\n");
if ((ssaStatusOctet & SSA_RESULT_HIGHER_THAN_PATIENT_HIGH_LEVEL) > 0)
builder.append("- Result Higher then Patient High Level\n");
if ((ssaStatusOctet & SSA_RESULT_LOWER_THAN_HYPO_LEVEL) > 0)
builder.append("- Result Lower then Hypo Level\n");
if ((ssaStatusOctet & SSA_RESULT_HIGHER_THAN_HYPER_LEVEL) > 0)
builder.append("- Result Higher then Hyper Level\n");
if ((ssaStatusOctet & SSA_SENSOR_RATE_OF_DECREASE_EXCEEDED) > 0)
builder.append("- Sensor Rate of Decrease Exceeded\n");
if ((ssaStatusOctet & SSA_SENSOR_RATE_OF_INCREASE_EXCEEDED) > 0)
builder.append("- Sensor Rate of Increase Exceeded\n");
if ((ssaStatusOctet & SSA_RESULT_LOWER_THAN_DEVICE_CAN_PROCESS) > 0)
builder.append("- Result Lower then Device Can Process\n");
if ((ssaStatusOctet & SSA_RESULT_HIGHER_THAN_DEVICE_CAN_PROCESS) > 0)
builder.append("- Result Higher then Device Can Process\n");
}
if (cgmTrendInformationPresent) {
final float trend = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
offset += 2;
builder.append("Trend: ").append(trend).append(" mg/dL/min\n");
}
if (cgmQualityPresent) {
final float quality = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
offset += 2;
builder.append("Quality: ").append(quality).append("%\n");
}
if (size > offset + 1) {
final int crc = data.getIntValue(Data.FORMAT_UINT16, offset);
// offset += 2;
builder.append(String.format(Locale.US, "E2E-CRC: 0x%04X\n", crc));
}
builder.setLength(builder.length() - 1); // Remove last \n
return size;
}
}

View File

@@ -0,0 +1,295 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class CGMSpecificOpsControlPointParser {
private final static int OP_SET_CGM_COMMUNICATION_INTERVAL = 1;
private final static int OP_GET_CGM_COMMUNICATION_INTERVAL = 2;
private final static int OP_CGM_COMMUNICATION_INTERVAL_RESPONSE = 3;
private final static int OP_SET_GLUCOSE_CALIBRATION_VALUE = 4;
private final static int OP_GET_GLUCOSE_CALIBRATION_VALUE = 5;
private final static int OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE = 6;
private final static int OP_SET_PATIENT_HIGH_ALERT_LEVEL = 7;
private final static int OP_GET_PATIENT_HIGH_ALERT_LEVEL = 8;
private final static int OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE = 9;
private final static int OP_SET_PATIENT_LOW_ALERT_LEVEL = 10;
private final static int OP_GET_PATIENT_LOW_ALERT_LEVEL = 11;
private final static int OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE = 12;
private final static int OP_SET_HYPO_ALERT_LEVEL = 13;
private final static int OP_GET_HYPO_ALERT_LEVEL = 14;
private final static int OP_HYPO_ALERT_LEVEL_RESPONSE = 15;
private final static int OP_SET_HYPER_ALERT_LEVEL = 16;
private final static int OP_GET_HYPER_ALERT_LEVEL = 17;
private final static int OP_HYPER_ALERT_LEVEL_RESPONSE = 18;
private final static int OP_SET_RATE_OF_DECREASE_ALERT_LEVEL = 19;
private final static int OP_GET_RATE_OF_DECREASE_ALERT_LEVEL = 20;
private final static int OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE = 21;
private final static int OP_SET_RATE_OF_INCREASE_ALERT_LEVEL = 22;
private final static int OP_GET_RATE_OF_INCREASE_ALERT_LEVEL = 23;
private final static int OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE = 24;
private final static int OP_RESET_DEVICE_SPECIFIC_ALERT = 25;
private final static int OP_CODE_START_SESSION = 26;
private final static int OP_CODE_STOP_SESSION = 27;
private final static int OP_CODE_RESPONSE_CODE = 28;
// TODO this parser does not support E2E-CRC!
public static String parse(final Data data) {
int offset = 0;
final int opCode = data.getIntValue(Data.FORMAT_UINT8, offset++);
final StringBuilder builder = new StringBuilder();
builder.append(parseOpCode(opCode));
switch (opCode) {
case OP_SET_CGM_COMMUNICATION_INTERVAL:
case OP_CGM_COMMUNICATION_INTERVAL_RESPONSE: {
final int interval = data.getIntValue(Data.FORMAT_UINT8, offset);
builder.append(" to ").append(interval).append(" min");
break;
}
case OP_SET_GLUCOSE_CALIBRATION_VALUE: {
final float calConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
offset += 2;
final int calTime = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
final int calTypeSampleLocation = data.getIntValue(Data.FORMAT_UINT8, offset++);
final int calType = calTypeSampleLocation & 0x0F;
final int calSampleLocation = (calTypeSampleLocation & 0xF0) >> 4;
final int calNextCalibrationTime = data.getIntValue(Data.FORMAT_UINT16, offset);
// offset += 2;
// final int calCalibrationDataRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset);
// offset += 2;
// final int calStatus = data.getIntValue(Data.FORMAT_UINT8, offset++);
builder.append(" to:\n");
builder.append("Glucose Concentration of Calibration: ").append(calConcentration).append(" mg/dL\n");
builder.append("Time: ").append(calTime).append(" min\n");
builder.append("Type: ").append(parseType(calType)).append("\n");
builder.append("Sample Location: ").append(parseSampleLocation(calSampleLocation)).append("\n");
builder.append("Next Calibration Time: ").append(parseNextCalibrationTime(calNextCalibrationTime)).append(" min\n"); // field ignored on Set
// builder.append("Data Record Number: ").append(calCalibrationDataRecordNumber).append("\n"); // field ignored on Set
// parseStatus(builder, calStatus); // field ignored on Set
break;
}
case OP_GET_GLUCOSE_CALIBRATION_VALUE: {
final int calibrationRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset);
builder.append(": ").append(parseRecordNumber(calibrationRecordNumber));
break;
}
case OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE: {
final float calConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
offset += 2;
final int calTime = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
final int calTypeSampleLocation = data.getIntValue(Data.FORMAT_UINT8, offset++);
final int calType = calTypeSampleLocation & 0x0F;
final int calSampleLocation = (calTypeSampleLocation & 0xF0) >> 4;
final int calNextCalibrationTime = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
final int calCalibrationDataRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
final int calStatus = data.getIntValue(Data.FORMAT_UINT8, offset);
builder.append(":\n");
if (calCalibrationDataRecordNumber > 0) {
builder.append("Glucose Concentration of Calibration: ").append(calConcentration).append(" mg/dL\n");
builder.append("Time: ").append(calTime).append(" min\n");
builder.append("Type: ").append(parseType(calType)).append("\n");
builder.append("Sample Location: ").append(parseSampleLocation(calSampleLocation)).append("\n");
builder.append("Next Calibration Time: ").append(parseNextCalibrationTime(calNextCalibrationTime)).append("\n");
builder.append("Data Record Number: ").append(calCalibrationDataRecordNumber);
parseStatus(builder, calStatus);
} else {
builder.append("No Calibration Data Stored");
}
break;
}
case OP_SET_PATIENT_HIGH_ALERT_LEVEL:
case OP_SET_PATIENT_LOW_ALERT_LEVEL:
case OP_SET_HYPO_ALERT_LEVEL:
case OP_SET_HYPER_ALERT_LEVEL: {
final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
builder.append(" to: ").append(level).append(" mg/dL");
break;
}
case OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE:
case OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE:
case OP_HYPO_ALERT_LEVEL_RESPONSE:
case OP_HYPER_ALERT_LEVEL_RESPONSE: {
final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
builder.append(": ").append(level).append(" mg/dL");
break;
}
case OP_SET_RATE_OF_DECREASE_ALERT_LEVEL:
case OP_SET_RATE_OF_INCREASE_ALERT_LEVEL: {
final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
builder.append(" to: ").append(level).append(" mg/dL/min");
break;
}
case OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE:
case OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE: {
final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
builder.append(": ").append(level).append(" mg/dL/min");
break;
}
case OP_CODE_RESPONSE_CODE:
final int requestOpCode = data.getIntValue(Data.FORMAT_UINT8, offset++);
final int responseCode = data.getIntValue(Data.FORMAT_UINT8, offset++);
builder.append(" to ").append(parseOpCode(requestOpCode)).append(": ").append(parseResponseCode(responseCode));
break;
}
return builder.toString();
}
private static String parseOpCode(final int code) {
switch (code) {
case OP_SET_CGM_COMMUNICATION_INTERVAL:
return "Set CGM Communication Interval";
case OP_GET_CGM_COMMUNICATION_INTERVAL:
return "Get CGM Communication Interval";
case OP_CGM_COMMUNICATION_INTERVAL_RESPONSE:
return "CGM Communication Interval";
case OP_SET_GLUCOSE_CALIBRATION_VALUE:
return "Set CGM Calibration Value";
case OP_GET_GLUCOSE_CALIBRATION_VALUE:
return "Get CGM Calibration Value";
case OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE:
return "CGM Calibration Value";
case OP_SET_PATIENT_HIGH_ALERT_LEVEL:
return "Set Patient High Alert Level";
case OP_GET_PATIENT_HIGH_ALERT_LEVEL:
return "Get Patient High Alert Level";
case OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE:
return "Patient High Alert Level";
case OP_SET_PATIENT_LOW_ALERT_LEVEL:
return "Set Patient Low Alert Level";
case OP_GET_PATIENT_LOW_ALERT_LEVEL:
return "Get Patient Low Alert Level";
case OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE:
return "Patient Low Alert Level";
case OP_SET_HYPO_ALERT_LEVEL:
return "Set Hypo Alert Level";
case OP_GET_HYPO_ALERT_LEVEL:
return "Get Hypo Alert Level";
case OP_HYPO_ALERT_LEVEL_RESPONSE:
return "Hypo Alert Level";
case OP_SET_HYPER_ALERT_LEVEL:
return "Set Hyper Alert Level";
case OP_GET_HYPER_ALERT_LEVEL:
return "Get Hyper Alert Level";
case OP_HYPER_ALERT_LEVEL_RESPONSE:
return "Hyper Alert Level";
case OP_SET_RATE_OF_DECREASE_ALERT_LEVEL:
return "Set Rate of Decrease Alert Level";
case OP_GET_RATE_OF_DECREASE_ALERT_LEVEL:
return "Get Rate of Decrease Alert Level";
case OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE:
return "Rate of Decrease Alert Level";
case OP_SET_RATE_OF_INCREASE_ALERT_LEVEL:
return "Set Rate of Increase Alert Level";
case OP_GET_RATE_OF_INCREASE_ALERT_LEVEL:
return "Get Rate of Increase Alert Level";
case OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE:
return "Rate of Increase Alert Level";
case OP_RESET_DEVICE_SPECIFIC_ALERT:
return "Reset Device Specific Alert";
case OP_CODE_START_SESSION:
return "Start Session";
case OP_CODE_STOP_SESSION:
return "Stop Session";
case OP_CODE_RESPONSE_CODE:
return "Response";
default:
return "Reserved for future use (" + code + ")";
}
}
private static String parseResponseCode(final int code) {
switch (code) {
case 1: return "Success";
case 2: return "Op Code not supported";
case 3: return "Invalid Operand";
case 4: return "Procedure not completed";
case 5: return "Parameter out of range";
default:
return "Reserved for future use (" + code + ")";
}
}
private static String parseType(final int type) {
switch (type) {
case 1: return "Capillary Whole blood";
case 2: return "Capillary Plasma";
case 3: return "Capillary Whole blood";
case 4: return "Venous Plasma";
case 5: return "Arterial Whole blood";
case 6: return "Arterial Plasma";
case 7: return "Undetermined Whole blood";
case 8: return "Undetermined Plasma";
case 9: return "Interstitial Fluid (ISF)";
case 10: return "Control Solution";
default: return "Reserved for future use (" + type + ")";
}
}
private static String parseSampleLocation(final int location) {
switch (location) {
case 1: return "Finger";
case 2: return "Alternate Site Test (AST)";
case 3: return "Earlobe";
case 4: return "Control solution";
case 5: return "Subcutaneous tissue";
case 15: return "Sample Location value not available";
default: return "Reserved for future use (" + location + ")";
}
}
private static String parseNextCalibrationTime(final int time) {
if (time == 0)
return "Calibration Required Instantly";
return time + " min";
}
private static String parseRecordNumber(final int time) {
if (time == 0xFFFF)
return "Last Calibration Data";
return String.valueOf(time);
}
private static void parseStatus(final StringBuilder builder, final int status) {
if (status == 0)
return;
builder.append("\nStatus:\n");
if ((status & 1) > 0)
builder.append("- Calibration Data rejected");
if ((status & 2) > 0)
builder.append("- Calibration Data out of range");
if ((status & 4) > 0)
builder.append("- Calibration Process pending");
if ((status & 0xF8) > 0)
builder.append("- Reserved for future use (").append(status).append(")");
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class CSCMeasurementParser {
private static final byte WHEEL_REV_DATA_PRESENT = 0x01; // 1 bit
private static final byte CRANK_REV_DATA_PRESENT = 0x02; // 1 bit
public static String parse(final Data data) {
int offset = 0;
final int flags = data.getByte(offset); // 1 byte
offset += 1;
final boolean wheelRevPresent = (flags & WHEEL_REV_DATA_PRESENT) > 0;
final boolean crankRevPreset = (flags & CRANK_REV_DATA_PRESENT) > 0;
int wheelRevolutions = 0;
int lastWheelEventTime = 0;
if (wheelRevPresent) {
wheelRevolutions = data.getIntValue(Data.FORMAT_UINT32, offset);
offset += 4;
lastWheelEventTime = data.getIntValue(Data.FORMAT_UINT16, offset); // 1/1024 s
offset += 2;
}
int crankRevolutions = 0;
int lastCrankEventTime = 0;
if (crankRevPreset) {
crankRevolutions = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
lastCrankEventTime = data.getIntValue(Data.FORMAT_UINT16, offset);
//offset += 2;
}
final StringBuilder builder = new StringBuilder();
if (wheelRevPresent) {
builder.append("Wheel rev: ").append(wheelRevolutions).append(",\n");
builder.append("Last wheel event time: ").append(lastWheelEventTime).append(",\n");
}
if (crankRevPreset) {
builder.append("Crank rev: ").append(crankRevolutions).append(",\n");
builder.append("Last crank event time: ").append(lastCrankEventTime).append(",\n");
}
if (!wheelRevPresent && !crankRevPreset) {
builder.append("No wheel or crank data");
}
builder.setLength(builder.length() - 2);
return builder.toString();
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.parser;
import java.util.Calendar;
import java.util.Locale;
import no.nordicsemi.android.ble.common.callback.DateTimeDataCallback;
import no.nordicsemi.android.ble.data.Data;
public class DateTimeParser {
/**
* Parses the date and time info.
*
* @param data
* @return time in human readable format
*/
public static String parse(final Data data) {
return parse(data, 0);
}
/**
* Parses the date and time info. This data has 7 bytes
*
* @param data
* @param offset
* offset to start reading the time
* @return time in human readable format
*/
/* package */static String parse(final Data data, final int offset) {
final Calendar calendar = DateTimeDataCallback.readDateTime(data, offset);
return String.format(Locale.US, "%1$te %1$tb %1$tY, %1$tH:%1$tM:%1$tS", calendar);
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class GlucoseMeasurementContextParser {
private static final int UNIT_kg = 0;
private static final int UNIT_l = 1;
public static String parse(final Data data) {
final StringBuilder builder = new StringBuilder();
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset);
offset += 1;
final boolean carbohydratePresent = (flags & 0x01) > 0;
final boolean mealPresent = (flags & 0x02) > 0;
final boolean testerHealthPresent = (flags & 0x04) > 0;
final boolean exercisePresent = (flags & 0x08) > 0;
final boolean medicationPresent = (flags & 0x10) > 0;
final int medicationUnit = (flags & 0x20) > 0 ? UNIT_l : UNIT_kg;
final boolean hbA1cPresent = (flags & 0x40) > 0;
final boolean moreFlagsPresent = (flags & 0x80) > 0;
final int sequenceNumber = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
if (moreFlagsPresent) // not supported yet
offset += 1;
builder.append("Sequence number: ").append(sequenceNumber);
if (carbohydratePresent) {
final int carbohydrateId = data.getIntValue(Data.FORMAT_UINT8, offset);
final float carbohydrateUnits = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 1);
builder.append("\nCarbohydrate: ").append(getCarbohydrate(carbohydrateId)).append(" (").append(carbohydrateUnits).append(carbohydrateUnits == UNIT_kg ? "kg" : "l").append(")");
offset += 3;
}
if (mealPresent) {
final int meal = data.getIntValue(Data.FORMAT_UINT8, offset);
builder.append("\nMeal: ").append(getMeal(meal));
offset += 1;
}
if (testerHealthPresent) {
final int testerHealth = data.getIntValue(Data.FORMAT_UINT8, offset);
final int tester = (testerHealth & 0xF0) >> 4;
final int health = (testerHealth & 0x0F);
builder.append("\nTester: ").append(getTester(tester));
builder.append("\nHealth: ").append(getHealth(health));
offset += 1;
}
if (exercisePresent) {
final int exerciseDuration = data.getIntValue(Data.FORMAT_UINT16, offset);
final int exerciseIntensity = data.getIntValue(Data.FORMAT_UINT8, offset + 2);
builder.append("\nExercise duration: ").append(exerciseDuration).append("s (intensity ").append(exerciseIntensity).append("%)");
offset += 3;
}
if (medicationPresent) {
final int medicationId = data.getIntValue(Data.FORMAT_UINT8, offset);
final float medicationQuantity = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 1);
builder.append("\nMedication: ").append(getMedicationId(medicationId)).append(" (").append(medicationQuantity).append(medicationUnit == UNIT_kg ? "kg" : "l");
offset += 3;
}
if (hbA1cPresent) {
final float HbA1c = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
builder.append("\nHbA1c: ").append(HbA1c).append("%");
}
return builder.toString();
}
private static String getCarbohydrate(final int id) {
switch (id) {
case 1:
return "Breakfast";
case 2:
return "Lunch";
case 3:
return "Dinner";
case 4:
return "Snack";
case 5:
return "Drink";
case 6:
return "Supper";
case 7:
return "Brunch";
default:
return "Reserved for future use (" + id + ")";
}
}
private static String getMeal(final int id) {
switch (id) {
case 1:
return "Preprandial (before meal)";
case 2:
return "Postprandial (after meal)";
case 3:
return "Fasting";
case 4:
return "Casual (snacks, drinks, etc.)";
case 5:
return "Bedtime";
default:
return "Reserved for future use (" + id + ")";
}
}
private static String getTester(final int id) {
switch (id) {
case 1:
return "Self";
case 2:
return "Health Care Professional";
case 3:
return "Lab test";
case 4:
return "Casual (snacks, drinks, etc.)";
case 15:
return "Tester value not available";
default:
return "Reserved for future use (" + id + ")";
}
}
private static String getHealth(final int id) {
switch (id) {
case 1:
return "Minor health issues";
case 2:
return "Major health issues";
case 3:
return "During menses";
case 4:
return "Under stress";
case 5:
return "No health issues";
case 15:
return "Health value not available";
default:
return "Reserved for future use (" + id + ")";
}
}
private static String getMedicationId(final int id) {
switch (id) {
case 1:
return "Rapid acting insulin";
case 2:
return "Short acting insulin";
case 3:
return "Intermediate acting insulin";
case 4:
return "Long acting insulin";
case 5:
return "Pre-mixed insulin";
default:
return "Reserved for future use (" + id + ")";
}
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class GlucoseMeasurementParser {
private static final int UNIT_kgpl = 0;
private static final int UNIT_molpl = 1;
private static final int STATUS_DEVICE_BATTERY_LOW = 0x0001;
private static final int STATUS_SENSOR_MALFUNCTION = 0x0002;
private static final int STATUS_SAMPLE_SIZE_FOR_BLOOD_OR_CONTROL_SOLUTION_INSUFFICIENT = 0x0004;
private static final int STATUS_STRIP_INSERTION_ERROR = 0x0008;
private static final int STATUS_STRIP_TYPE_INCORRECT_FOR_DEVICE = 0x0010;
private static final int STATUS_SENSOR_RESULT_TOO_HIGH = 0x0020;
private static final int STATUS_SENSOR_RESULT_TOO_LOW = 0x0040;
private static final int STATUS_SENSOR_TEMPERATURE_TOO_HIGH = 0x0080;
private static final int STATUS_SENSOR_TEMPERATURE_TOO_LOW = 0x0100;
private static final int STATUS_SENSOR_READ_INTERRUPTED = 0x0200;
private static final int STATUS_GENERAL_DEVICE_FAULT = 0x0400;
private static final int STATUS_TIME_FAULT = 0x0800;
public static String parse(final Data data) {
final StringBuilder builder = new StringBuilder();
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset);
offset += 1;
final boolean timeOffsetPresent = (flags & 0x01) > 0;
final boolean typeAndLocationPresent = (flags & 0x02) > 0;
final int concentrationUnit = (flags & 0x04) > 0 ? UNIT_molpl : UNIT_kgpl;
final boolean sensorStatusAnnunciationPresent = (flags & 0x08) > 0;
final boolean contextInfoFollows = (flags & 0x10) > 0;
// create and fill the new record
final int sequenceNumber = data.getIntValue(Data.FORMAT_UINT16, offset);
builder.append("Sequence Number: ").append(sequenceNumber);
offset += 2;
builder.append("\nBase Time: ").append(DateTimeParser.parse(data, offset));
offset += 7;
if (timeOffsetPresent) {
// time offset is ignored in the current release
final int timeOffset = data.getIntValue(Data.FORMAT_SINT16, offset);
builder.append("\nTime Offset: ").append(timeOffset).append(" min");
offset += 2;
}
if (typeAndLocationPresent) {
final float glucoseConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
final int typeAndLocation = data.getIntValue(Data.FORMAT_UINT8, offset + 2);
final int type = (typeAndLocation & 0xF0) >> 4; // TODO this way or around?
final int sampleLocation = (typeAndLocation & 0x0F);
builder.append("\nGlucose Concentration: ").append(glucoseConcentration).append(concentrationUnit == UNIT_kgpl ? " kg/l" : " mol/l");
builder.append("\nSample Type: ").append(getType(type));
builder.append("\nSample Location: ").append(getLocation(sampleLocation));
offset += 3;
}
if (sensorStatusAnnunciationPresent) {
final int status = data.getIntValue(Data.FORMAT_UINT16, offset);
builder.append("Status:\n").append(getStatusAnnunciation(status));
}
builder.append("\nContext information follows: ").append(contextInfoFollows);
return builder.toString();
}
private static String getType(final int type) {
switch (type) {
case 1:
return "Capillary Whole blood";
case 2:
return "Capillary Plasma";
case 3:
return "Venous Whole blood";
case 4:
return "Venous Plasma";
case 5:
return "Arterial Whole blood";
case 6:
return "Arterial Plasma";
case 7:
return "Undetermined Whole blood";
case 8:
return "Undetermined Plasma";
case 9:
return "Interstitial Fluid (ISF)";
case 10:
return "Control Solution";
default:
return "Reserved for future use (" + type + ")";
}
}
private static String getLocation(final int location) {
switch (location) {
case 1:
return "Finger";
case 2:
return "Alternate Site Test (AST)";
case 3:
return "Earlobe";
case 4:
return "Control solution";
case 15:
return "Value not available";
default:
return "Reserved for future use (" + location + ")";
}
}
private static String getStatusAnnunciation(final int status) {
final StringBuilder builder = new StringBuilder();
if ((status & STATUS_DEVICE_BATTERY_LOW) > 0)
builder.append("\nDevice battery low at time of measurement");
if ((status & STATUS_SENSOR_MALFUNCTION) > 0)
builder.append("\nSensor malfunction or faulting at time of measurement");
if ((status & STATUS_SAMPLE_SIZE_FOR_BLOOD_OR_CONTROL_SOLUTION_INSUFFICIENT) > 0)
builder.append("\nSample size for blood or control solution insufficient at time of measurement");
if ((status & STATUS_STRIP_INSERTION_ERROR) > 0)
builder.append("\nStrip insertion error");
if ((status & STATUS_STRIP_TYPE_INCORRECT_FOR_DEVICE) > 0)
builder.append("\nStrip type incorrect for device");
if ((status & STATUS_SENSOR_RESULT_TOO_HIGH) > 0)
builder.append("\nSensor result higher than the device can process");
if ((status & STATUS_SENSOR_RESULT_TOO_LOW) > 0)
builder.append("\nSensor result lower than the device can process");
if ((status & STATUS_SENSOR_TEMPERATURE_TOO_HIGH) > 0)
builder.append("\nSensor temperature too high for valid test/result at time of measurement");
if ((status & STATUS_SENSOR_TEMPERATURE_TOO_LOW) > 0)
builder.append("\nSensor temperature too low for valid test/result at time of measurement");
if ((status & STATUS_SENSOR_READ_INTERRUPTED) > 0)
builder.append("\nSensor read interrupted because strip was pulled too soon at time of measurement");
if ((status & STATUS_GENERAL_DEVICE_FAULT) > 0)
builder.append("\nGeneral device fault has occurred in the sensor");
if ((status & STATUS_TIME_FAULT) > 0)
builder.append("\nTime fault has occurred in the sensor and time may be inaccurate");
return builder.toString();
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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.parser;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class HeartRateMeasurementParser {
private static final byte HEART_RATE_VALUE_FORMAT = 0x01; // 1 bit
private static final byte SENSOR_CONTACT_STATUS = 0x06; // 2 bits
private static final byte ENERGY_EXPANDED_STATUS = 0x08; // 1 bit
private static final byte RR_INTERVAL = 0x10; // 1 bit
public static String parse(final Data data) {
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
/*
* false Heart Rate Value Format is set to UINT8. Units: beats per minute (bpm)
* true Heart Rate Value Format is set to UINT16. Units: beats per minute (bpm)
*/
final boolean value16bit = (flags & HEART_RATE_VALUE_FORMAT) > 0;
/*
* 0 Sensor Contact feature is not supported in the current connection
* 1 Sensor Contact feature is not supported in the current connection
* 2 Sensor Contact feature is supported, but contact is not detected
* 3 Sensor Contact feature is supported and contact is detected
*/
final int sensorContactStatus = (flags & SENSOR_CONTACT_STATUS) >> 1;
/*
* false Energy Expended field is not present
* true Energy Expended field is present. Units: kilo Joules
*/
final boolean energyExpandedStatus = (flags & ENERGY_EXPANDED_STATUS) > 0;
/*
* false RR-Interval values are not present.
* true One or more RR-Interval values are present. Units: 1/1024 seconds
*/
final boolean rrIntervalStatus = (flags & RR_INTERVAL) > 0;
// heart rate value is 8 or 16 bit long
int heartRateValue = data.getIntValue(value16bit ? Data.FORMAT_UINT16 : Data.FORMAT_UINT8, offset++); // bits per minute
if (value16bit)
offset++;
// energy expanded value is present if a flag was set
int energyExpanded = -1;
if (energyExpandedStatus)
energyExpanded = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
// RR-interval is set when a flag is set
final List<Float> rrIntervals = new ArrayList<>();
if (rrIntervalStatus) {
for (int o = offset; o < data.getValue().length; o += 2) {
final int units = data.getIntValue(Data.FORMAT_UINT16, o);
rrIntervals.add(units * 1000.0f / 1024.0f); // RR interval is in [1/1024s]
}
}
final StringBuilder builder = new StringBuilder();
builder.append("Heart Rate Measurement: ").append(heartRateValue).append(" bpm");
switch (sensorContactStatus) {
case 0:
case 1:
builder.append(",\nSensor Contact Not Supported");
break;
case 2:
builder.append(",\nContact is NOT Detected");
break;
case 3:
builder.append(",\nContact is Detected");
break;
}
if (energyExpandedStatus)
builder.append(",\nEnergy Expanded: ").append(energyExpanded).append(" kJ");
if (rrIntervalStatus) {
builder.append(",\nRR Interval: ");
for (final Float interval : rrIntervals)
builder.append(String.format(Locale.US, "%.02f ms, ", interval));
builder.setLength(builder.length() - 2); // remove the ", " at the end
}
return builder.toString();
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class IntermediateCuffPressureParser {
public static String parse(final Data data) {
final StringBuilder builder = new StringBuilder();
// first byte - flags
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
final int unitType = flags & 0x01;
final boolean timestampPresent = (flags & 0x02) > 0;
final boolean pulseRatePresent = (flags & 0x04) > 0;
final boolean userIdPresent = (flags & 0x08) > 0;
final boolean statusPresent = (flags & 0x10) > 0;
// following bytes - pressure
final float pressure = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
final String unit = unitType == 0 ? " mmHg" : " kPa";
offset += 6;
builder.append("Cuff pressure: ").append(pressure).append(unit);
// parse timestamp if present
if (timestampPresent) {
builder.append("Timestamp: ").append(DateTimeParser.parse(data, offset));
offset += 7;
}
// parse pulse rate if present
if (pulseRatePresent) {
final float pulseRate = data.getFloatValue(Data.FORMAT_SFLOAT, offset);
offset += 2;
builder.append("\nPulse: ").append(pulseRate).append(" bpm");
}
if (userIdPresent) {
final int userId = data.getIntValue(Data.FORMAT_UINT8, offset);
offset += 1;
builder.append("\nUser ID: ").append(userId);
}
if (statusPresent) {
final int status = data.getIntValue(Data.FORMAT_UINT16, offset);
// offset += 2;
if ((status & 0x0001) > 0)
builder.append("\nBody movement detected");
if ((status & 0x0002) > 0)
builder.append("\nCuff too lose");
if ((status & 0x0004) > 0)
builder.append("\nIrregular pulse detected");
if ((status & 0x0018) == 0x0008)
builder.append("\nPulse rate exceeds upper limit");
if ((status & 0x0018) == 0x0010)
builder.append("\nPulse rate is less than lower limit");
if ((status & 0x0018) == 0x0018)
builder.append("\nPulse rate range: Reserved for future use ");
if ((status & 0x0020) > 0)
builder.append("\nImproper measurement position");
}
return builder.toString();
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.parser;
import java.util.Locale;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class RSCMeasurementParser {
private static final byte INSTANTANEOUS_STRIDE_LENGTH_PRESENT = 0x01; // 1 bit
private static final byte TOTAL_DISTANCE_PRESENT = 0x02; // 1 bit
private static final byte WALKING_OR_RUNNING_STATUS_BITS = 0x04; // 1 bit
public static String parse(final Data data) {
int offset = 0;
final int flags = data.getValue()[offset]; // 1 byte
offset += 1;
final boolean islmPresent = (flags & INSTANTANEOUS_STRIDE_LENGTH_PRESENT) > 0;
final boolean tdPreset = (flags & TOTAL_DISTANCE_PRESENT) > 0;
final boolean running = (flags & WALKING_OR_RUNNING_STATUS_BITS) > 0;
final boolean walking = !running;
final float instantaneousSpeed = (float) data.getIntValue(Data.FORMAT_UINT16, offset) / 256.0f; // 1/256 m/s
offset += 2;
final int instantaneousCadence = data.getIntValue(Data.FORMAT_UINT8, offset);
offset += 1;
float instantaneousStrideLength = 0;
if (islmPresent) {
instantaneousStrideLength = (float) data.getIntValue(Data.FORMAT_UINT16, offset) / 100.0f; // 1/100 m
offset += 2;
}
float totalDistance = 0;
if (tdPreset) {
totalDistance = (float) data.getIntValue(Data.FORMAT_UINT32, offset) / 10.0f;
// offset += 4;
}
final StringBuilder builder = new StringBuilder();
builder.append(String.format(Locale.US, "Speed: %.2f m/s, Cadence: %d RPM,\n", instantaneousSpeed, instantaneousCadence));
if (islmPresent)
builder.append(String.format(Locale.US, "Instantaneous Stride Length: %.2f m,\n", instantaneousStrideLength));
if (tdPreset)
builder.append(String.format(Locale.US, "Total Distance: %.1f m,\n", totalDistance));
if (walking)
builder.append("Status: WALKING");
else
builder.append("Status: RUNNING");
return builder.toString();
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class RecordAccessControlPointParser {
private final static int OP_CODE_REPORT_STORED_RECORDS = 1;
private final static int OP_CODE_DELETE_STORED_RECORDS = 2;
private final static int OP_CODE_ABORT_OPERATION = 3;
private final static int OP_CODE_REPORT_NUMBER_OF_RECORDS = 4;
private final static int OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5;
private final static int OP_CODE_RESPONSE_CODE = 6;
private final static int OPERATOR_NULL = 0;
private final static int OPERATOR_ALL_RECORDS = 1;
private final static int OPERATOR_LESS_THEN_OR_EQUAL = 2;
private final static int OPERATOR_GREATER_THEN_OR_EQUAL = 3;
private final static int OPERATOR_WITHING_RANGE = 4;
private final static int OPERATOR_FIRST_RECORD = 5;
private final static int OPERATOR_LAST_RECORD = 6;
private final static int RESPONSE_SUCCESS = 1;
private final static int RESPONSE_OP_CODE_NOT_SUPPORTED = 2;
private final static int RESPONSE_INVALID_OPERATOR = 3;
private final static int RESPONSE_OPERATOR_NOT_SUPPORTED = 4;
private final static int RESPONSE_INVALID_OPERAND = 5;
private final static int RESPONSE_NO_RECORDS_FOUND = 6;
private final static int RESPONSE_ABORT_UNSUCCESSFUL = 7;
private final static int RESPONSE_PROCEDURE_NOT_COMPLETED = 8;
private final static int RESPONSE_OPERAND_NOT_SUPPORTED = 9;
public static String parse(final Data data) {
final StringBuilder builder = new StringBuilder();
final int opCode = data.getIntValue(Data.FORMAT_UINT8, 0);
final int operator = data.getIntValue(Data.FORMAT_UINT8, 1);
switch (opCode) {
case OP_CODE_REPORT_STORED_RECORDS:
case OP_CODE_DELETE_STORED_RECORDS:
case OP_CODE_ABORT_OPERATION:
case OP_CODE_REPORT_NUMBER_OF_RECORDS:
builder.append(getOpCode(opCode)).append("\n");
break;
case OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE: {
builder.append(getOpCode(opCode)).append(": ");
final int value = data.getIntValue(Data.FORMAT_UINT16, 2);
builder.append(value).append("\n");
break;
}
case OP_CODE_RESPONSE_CODE: {
builder.append(getOpCode(opCode)).append(" for ");
final int targetOpCode = data.getIntValue(Data.FORMAT_UINT8, 2);
builder.append(getOpCode(targetOpCode)).append(": ");
final int status = data.getIntValue(Data.FORMAT_UINT8, 3);
builder.append(getStatus(status)).append("\n");
break;
}
}
switch (operator) {
case OPERATOR_ALL_RECORDS:
case OPERATOR_FIRST_RECORD:
case OPERATOR_LAST_RECORD:
builder.append("Operator: ").append(getOperator(operator)).append("\n");
break;
case OPERATOR_GREATER_THEN_OR_EQUAL:
case OPERATOR_LESS_THEN_OR_EQUAL: {
final int filter = data.getIntValue(Data.FORMAT_UINT8, 2);
final int value = data.getIntValue(Data.FORMAT_UINT16, 3);
builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value).append(" (filter: ").append(filter).append(")\n");
break;
}
case OPERATOR_WITHING_RANGE: {
final int filter = data.getIntValue(Data.FORMAT_UINT8, 2);
final int value1 = data.getIntValue(Data.FORMAT_UINT16, 3);
final int value2 = data.getIntValue(Data.FORMAT_UINT16, 5);
builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value1).append("-").append(value2).append(" (filter: ").append(filter).append(")\n");
break;
}
}
if (builder.length() > 0)
builder.setLength(builder.length() - 1);
return builder.toString();
}
private static String getOpCode(final int opCode) {
switch (opCode) {
case OP_CODE_REPORT_STORED_RECORDS:
return "Report stored records";
case OP_CODE_DELETE_STORED_RECORDS:
return "Delete stored records";
case OP_CODE_ABORT_OPERATION:
return "Abort operation";
case OP_CODE_REPORT_NUMBER_OF_RECORDS:
return "Report number of stored records";
case OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE:
return "Number of stored records response";
case OP_CODE_RESPONSE_CODE:
return "Response Code";
default:
return "Reserved for future use";
}
}
private static String getOperator(final int operator) {
switch (operator) {
case OPERATOR_NULL:
return "Null";
case OPERATOR_ALL_RECORDS:
return "All records";
case OPERATOR_LESS_THEN_OR_EQUAL:
return "Less than or equal to";
case OPERATOR_GREATER_THEN_OR_EQUAL:
return "Greater than or equal to";
case OPERATOR_WITHING_RANGE:
return "Within range of";
case OPERATOR_FIRST_RECORD:
return "First record(i.e. oldest record)";
case OPERATOR_LAST_RECORD:
return "Last record (i.e. most recent record)";
default:
return "Reserved for future use";
}
}
private static String getStatus(final int status) {
switch (status) {
case RESPONSE_SUCCESS:
return "Success";
case RESPONSE_OP_CODE_NOT_SUPPORTED:
return "Operation not supported";
case RESPONSE_INVALID_OPERATOR:
return "Invalid operator";
case RESPONSE_OPERATOR_NOT_SUPPORTED:
return "Operator not supported";
case RESPONSE_INVALID_OPERAND:
return "Invalid operand";
case RESPONSE_NO_RECORDS_FOUND:
return "No records found";
case RESPONSE_ABORT_UNSUCCESSFUL:
return "Abort unsuccessful";
case RESPONSE_PROCEDURE_NOT_COMPLETED:
return "Procedure not completed";
case RESPONSE_OPERAND_NOT_SUPPORTED:
return "Operand not supported";
default:
return "Reserved for future use";
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.parser;
import java.util.Locale;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class TemperatureMeasurementParser {
private static final byte TEMPERATURE_UNIT_FLAG = 0x01; // 1 bit
private static final byte TIMESTAMP_FLAG = 0x02; // 1 bits
private static final byte TEMPERATURE_TYPE_FLAG = 0x04; // 1 bit
public static String parse(final Data data) {
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
/*
* false Temperature is in Celsius degrees
* true Temperature is in Fahrenheit degrees
*/
final boolean fahrenheit = (flags & TEMPERATURE_UNIT_FLAG) > 0;
/*
* false No Timestamp in the packet
* true There is a timestamp information
*/
final boolean timestampIncluded = (flags & TIMESTAMP_FLAG) > 0;
/*
* false Temperature type is not included
* true Temperature type included in the packet
*/
final boolean temperatureTypeIncluded = (flags & TEMPERATURE_TYPE_FLAG) > 0;
final float tempValue = data.getFloatValue(Data.FORMAT_FLOAT, offset);
offset += 4;
String dateTime = null;
if (timestampIncluded) {
dateTime = DateTimeParser.parse(data, offset);
offset += 7;
}
String type = null;
if (temperatureTypeIncluded) {
type = TemperatureTypeParser.parse(data, offset);
// offset++;
}
final StringBuilder builder = new StringBuilder();
builder.append(String.format(Locale.US, "%.02f", tempValue));
if (fahrenheit)
builder.append("°F");
else
builder.append("°C");
if (timestampIncluded)
builder.append("\nTime: ").append(dateTime);
if (temperatureTypeIncluded)
builder.append("\nType: ").append(type);
return builder.toString();
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class TemperatureTypeParser {
public static String parse(final Data data) {
return parse(data, 0);
}
/* package */static String parse(final Data data, final int offset) {
final int type = data.getValue()[offset];
switch (type) {
case 1:
return "Armpit";
case 2:
return "Body (general)";
case 3:
return "Ear (usually ear lobe)";
case 4:
return "Finger";
case 5:
return "Gastro-intestinal Tract";
case 6:
return "Mouth";
case 7:
return "Rectum";
case 8:
return "Toe";
case 9:
return "Tympanum (ear drum)";
default:
return "Unknown";
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.parser;
import no.nordicsemi.android.ble.data.Data;
// TODO this method may be used for developing purposes to log the data from your device using the nRF Logger application.
@SuppressWarnings("ConstantConditions")
public class TemplateParser {
// TODO add some flags, if needed
private static final byte HEART_RATE_VALUE_FORMAT = 0x01; // 1 bit
/**
* This method converts the value of the characteristic to the String. The String is then logged in the nRF logger log session
* @param data the characteristic data to be parsed
* @return human readable value of the characteristic
*/
@SuppressWarnings("UnusedAssignment")
public static String parse(final Data data) {
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
/*
* In the template we are using the HRM values as an example.
* false Heart Rate Value Format is set to UINT8. Units: beats per minute (bpm)
* true Heart Rate Value Format is set to UINT16. Units: beats per minute (bpm)
*/
final boolean value16bit = (flags & HEART_RATE_VALUE_FORMAT) > 0;
// heart rate value is 8 or 16 bit long
int value = data.getIntValue(value16bit ? Data.FORMAT_UINT16 : Data.FORMAT_UINT8, offset++); // bits per minute
if (value16bit)
offset++;
// TODO parse more data
return "Template Measurement: " + value + " bpm";
}
}

View File

@@ -0,0 +1,440 @@
/*
* 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.profile;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.LocalLogSession;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.AppHelpFragment;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;
import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
@SuppressWarnings("unused")
public abstract class BleProfileActivity extends AppCompatActivity implements BleManagerCallbacks, ScannerFragment.OnDeviceSelectedListener {
private static final String TAG = "BaseProfileActivity";
private static final String SIS_CONNECTION_STATUS = "connection_status";
private static final String SIS_DEVICE_NAME = "device_name";
protected static final int REQUEST_ENABLE_BT = 2;
private LoggableBleManager<? extends BleManagerCallbacks> mBleManager;
private TextView mDeviceNameView;
private Button mConnectButton;
private ILogSession mLogSession;
private boolean mDeviceConnected = false;
private String mDeviceName;
@Override
protected final void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ensureBLESupported();
if (!isBLEEnabled()) {
showBLEDialog();
}
/*
* We use the managers using a singleton pattern. It's not recommended for the Android, because the singleton instance remains after Activity has been
* destroyed but it's simple and is used only for this demo purpose. In final application Managers should be created as a non-static objects in
* Services. The Service should implement ManagerCallbacks interface. The application Activity may communicate with such Service using binding,
* broadcast listeners, local broadcast listeners (see support.v4 library), or messages. See the Proximity profile for Service approach.
*/
mBleManager = initializeManager();
// In onInitialize method a final class may register local broadcast receivers that will listen for events from the service
onInitialize(savedInstanceState);
// The onCreateView class should... create the view
onCreateView(savedInstanceState);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
// Common nRF Toolbox view references are obtained here
setUpView();
// View is ready to be used
onViewCreated(savedInstanceState);
}
/**
* You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created.
*/
protected void onInitialize(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}.
* Use to obtain references to views. Connect/Disconnect button and the device name view are manager automatically.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
protected abstract void onCreateView(final Bundle savedInstanceState);
/**
* Called after the view has been created.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
protected void onViewCreated(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called after the view and the toolbar has been created.
*/
protected final void setUpView() {
// set GUI
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mConnectButton = findViewById(R.id.action_connect);
mDeviceNameView = findViewById(R.id.device_name);
}
@Override
public void onBackPressed() {
mBleManager.disconnect().enqueue();
super.onBackPressed();
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(SIS_CONNECTION_STATUS, mDeviceConnected);
outState.putString(SIS_DEVICE_NAME, mDeviceName);
}
@Override
protected void onRestoreInstanceState(final @NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mDeviceConnected = savedInstanceState.getBoolean(SIS_CONNECTION_STATUS);
mDeviceName = savedInstanceState.getString(SIS_DEVICE_NAME);
if (mDeviceConnected) {
mConnectButton.setText(R.string.action_disconnect);
} else {
mConnectButton.setText(R.string.action_connect);
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.help, menu);
return true;
}
/**
* Use this method to handle menu actions other than home and about.
*
* @param itemId the menu item id
* @return <code>true</code> if action has been handled
*/
protected boolean onOptionsItemSelected(final int itemId) {
// Overwrite when using menu other than R.menu.help
return false;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
onBackPressed();
break;
case R.id.action_about:
final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId());
fragment.show(getSupportFragmentManager(), "help_fragment");
break;
default:
return onOptionsItemSelected(id);
}
return true;
}
/**
* Called when user press CONNECT or DISCONNECT button. See layout files -> onClick attribute.
*/
public void onConnectClicked(final View view) {
if (isBLEEnabled()) {
if (!mDeviceConnected) {
setDefaultUI();
showDeviceScanningDialog(getFilterUUID());
} else {
mBleManager.disconnect().enqueue();
}
} else {
showBLEDialog();
}
}
/**
* Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used.
*
* @return the title resource id
*/
protected int getLoggerProfileTitle() {
return 0;
}
/**
* This method may return the local log content provider authority if local log sessions are supported.
*
* @return local log session content provider URI
*/
protected Uri getLocalAuthorityLogger() {
return null;
}
/**
* This method returns whether autoConnect option should be used.
*
* @return true to use autoConnect feature, false (default) otherwise.
*/
protected boolean shouldAutoConnect() {
return false;
}
@Override
public void onDeviceSelected(final BluetoothDevice device, final String name) {
final int titleId = getLoggerProfileTitle();
if (titleId > 0) {
mLogSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name);
// If nRF Logger is not installed we may want to use local logger
if (mLogSession == null && getLocalAuthorityLogger() != null) {
mLogSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name);
}
}
mDeviceName = name;
mBleManager.setLogger(mLogSession);
mBleManager.connect(device)
.useAutoConnect(shouldAutoConnect())
.retry(3, 100)
.enqueue();
}
@Override
public void onDialogCanceled() {
// do nothing
}
@Override
public void onDeviceConnecting(@NonNull final BluetoothDevice device) {
runOnUiThread(() -> {
mDeviceNameView.setText(mDeviceName != null ? mDeviceName : getString(R.string.not_available));
mConnectButton.setText(R.string.action_connecting);
});
}
@Override
public void onDeviceConnected(@NonNull final BluetoothDevice device) {
mDeviceConnected = true;
runOnUiThread(() -> mConnectButton.setText(R.string.action_disconnect));
}
@Override
public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) {
runOnUiThread(() -> mConnectButton.setText(R.string.action_disconnecting));
}
@Override
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
mDeviceConnected = false;
mBleManager.close();
runOnUiThread(() -> {
mConnectButton.setText(R.string.action_connect);
mDeviceNameView.setText(getDefaultDeviceName());
});
}
@Override
public void onLinkLossOccurred(@NonNull final BluetoothDevice device) {
mDeviceConnected = false;
}
@Override
public void onServicesDiscovered(@NonNull final BluetoothDevice device, boolean optionalServicesFound) {
// this may notify user or show some views
}
@Override
public void onDeviceReady(@NonNull final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBondingRequired(@NonNull final BluetoothDevice device) {
showToast(R.string.bonding);
}
@Override
public void onBonded(@NonNull final BluetoothDevice device) {
showToast(R.string.bonded);
}
@Override
public void onBondingFailed(@NonNull final BluetoothDevice device) {
showToast(R.string.bonding_failed);
}
@Override
public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) {
DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode);
showToast(message + " (" + errorCode + ")");
}
@Override
public void onDeviceNotSupported(@NonNull final BluetoothDevice device) {
showToast(R.string.not_supported);
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param message a message to be shown
*/
protected void showToast(final String message) {
runOnUiThread(() -> Toast.makeText(BleProfileActivity.this, message, Toast.LENGTH_SHORT).show());
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param messageResId an resource id of the message to be shown
*/
protected void showToast(final int messageResId) {
runOnUiThread(() -> Toast.makeText(BleProfileActivity.this, messageResId, Toast.LENGTH_SHORT).show());
}
/**
* Returns <code>true</code> if the device is connected. Services may not have been discovered yet.
*/
protected boolean isDeviceConnected() {
return mDeviceConnected;
}
/**
* Returns the name of the device that the phone is currently connected to or was connected last time
*/
protected String getDeviceName() {
return mDeviceName;
}
/**
* Initializes the Bluetooth Low Energy manager. A manager is used to communicate with profile's services.
*
* @return the manager that was created
*/
protected abstract LoggableBleManager<? extends BleManagerCallbacks> initializeManager();
/**
* Restores the default UI before reconnecting
*/
protected abstract void setDefaultUI();
/**
* Returns the default device name resource id. The real device name is obtained when connecting to the device. This one is used when device has
* disconnected.
*
* @return the default device name resource id
*/
protected abstract int getDefaultDeviceName();
/**
* Returns the string resource id that will be shown in About box
*
* @return the about resource id
*/
protected abstract int getAboutTextId();
/**
* The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also:
* {@link #isChangingConfigurations()}.
*
* @return the required UUID or <code>null</code>
*/
protected abstract UUID getFilterUUID();
/**
* Shows the scanner fragment.
*
* @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their
* services
* @see #getFilterUUID()
*/
private void showDeviceScanningDialog(final UUID filter) {
runOnUiThread(() -> {
final ScannerFragment dialog = ScannerFragment.getInstance(filter);
dialog.show(getSupportFragmentManager(), "scan_fragment");
});
}
/**
* Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT.
*
* @return the logger session or <code>null</code>
*/
protected ILogSession getLogSession() {
return mLogSession;
}
private void ensureBLESupported() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show();
finish();
}
}
protected boolean isBLEEnabled() {
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
final BluetoothAdapter adapter = bluetoothManager.getAdapter();
return adapter != null && adapter.isEnabled();
}
protected void showBLEDialog() {
final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
}

View File

@@ -0,0 +1,443 @@
/*
* 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.profile;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.LocalLogSession;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.AppHelpFragment;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.app.ExpandableListActivity;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;
import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
@SuppressWarnings("unused")
public abstract class BleProfileExpandableListActivity extends ExpandableListActivity implements BleManagerCallbacks, ScannerFragment.OnDeviceSelectedListener {
private static final String TAG = "BaseProfileActivity";
private static final String SIS_CONNECTION_STATUS = "connection_status";
private static final String SIS_DEVICE_NAME = "device_name";
protected static final int REQUEST_ENABLE_BT = 2;
private LoggableBleManager<? extends BleManagerCallbacks> mBleManager;
private TextView mDeviceNameView;
private Button mConnectButton;
private ILogSession mLogSession;
private boolean mDeviceConnected = false;
private String mDeviceName;
@Override
protected final void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ensureBLESupported();
if (!isBLEEnabled()) {
showBLEDialog();
}
/*
* We use the managers using a singleton pattern. It's not recommended for the Android, because the singleton instance remains after Activity has been
* destroyed but it's simple and is used only for this demo purpose. In final application Managers should be created as a non-static objects in
* Services. The Service should implement ManagerCallbacks interface. The application Activity may communicate with such Service using binding,
* broadcast listeners, local broadcast listeners (see support.v4 library), or messages. See the Proximity profile for Service approach.
*/
mBleManager = initializeManager();
// In onInitialize method a final class may register local broadcast receivers that will listen for events from the service
onInitialize(savedInstanceState);
// The onCreateView class should... create the view
onCreateView(savedInstanceState);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
// Common nRF Toolbox view references are obtained here
setUpView();
// View is ready to be used
onViewCreated(savedInstanceState);
}
/**
* You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created.
*/
@SuppressWarnings("unused")
protected void onInitialize(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}.
* Use to obtain references to views. Connect/Disconnect button and the device name view are manager automatically.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
protected abstract void onCreateView(final Bundle savedInstanceState);
/**
* Called after the view has been created.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
@SuppressWarnings("unused")
protected void onViewCreated(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called after the view and the toolbar has been created.
*/
protected final void setUpView() {
// set GUI
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mConnectButton = findViewById(R.id.action_connect);
mDeviceNameView = findViewById(R.id.device_name);
}
@Override
public void onBackPressed() {
mBleManager.disconnect().enqueue();
super.onBackPressed();
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(SIS_CONNECTION_STATUS, mDeviceConnected);
outState.putString(SIS_DEVICE_NAME, mDeviceName);
}
@Override
protected void onRestoreInstanceState(final @NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mDeviceConnected = savedInstanceState.getBoolean(SIS_CONNECTION_STATUS);
mDeviceName = savedInstanceState.getString(SIS_DEVICE_NAME);
if (mDeviceConnected) {
mConnectButton.setText(R.string.action_disconnect);
} else {
mConnectButton.setText(R.string.action_connect);
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.help, menu);
return true;
}
/**
* Use this method to handle menu actions other than home and about.
*
* @param itemId the menu item id
* @return <code>true</code> if action has been handled
*/
@SuppressWarnings("unused")
protected boolean onOptionsItemSelected(final int itemId) {
// Overwrite when using menu other than R.menu.help
return false;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
onBackPressed();
break;
case R.id.action_about:
final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId());
fragment.show(getSupportFragmentManager(), "help_fragment");
break;
default:
return onOptionsItemSelected(id);
}
return true;
}
/**
* Called when user press CONNECT or DISCONNECT button. See layout files -> onClick attribute.
*/
public void onConnectClicked(final View view) {
if (isBLEEnabled()) {
if (!mDeviceConnected) {
setDefaultUI();
showDeviceScanningDialog(getFilterUUID());
} else {
mBleManager.disconnect().enqueue();
}
} else {
showBLEDialog();
}
}
/**
* Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used.
*
* @return the title resource id
*/
protected int getLoggerProfileTitle() {
return 0;
}
/**
* This method may return the local log content provider authority if local log sessions are supported.
*
* @return local log session content provider URI
*/
protected Uri getLocalAuthorityLogger() {
return null;
}
/**
* This method returns whether autoConnect option should be used.
*
* @return true to use autoConnect feature, false (default) otherwise.
*/
protected boolean shouldAutoConnect() {
return false;
}
@Override
public void onDeviceSelected(final BluetoothDevice device, final String name) {
final int titleId = getLoggerProfileTitle();
if (titleId > 0) {
mLogSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name);
// If nRF Logger is not installed we may want to use local logger
if (mLogSession == null && getLocalAuthorityLogger() != null) {
mLogSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name);
}
}
mDeviceName = name;
mBleManager.setLogger(mLogSession);
mBleManager.connect(device)
.useAutoConnect(shouldAutoConnect())
.retry(3, 100)
.enqueue();
}
@Override
public void onDialogCanceled() {
// do nothing
}
@Override
public void onDeviceConnecting(@NonNull final BluetoothDevice device) {
runOnUiThread(() -> {
mDeviceNameView.setText(mDeviceName != null ? mDeviceName : getString(R.string.not_available));
mConnectButton.setText(R.string.action_connecting);
});
}
@Override
public void onDeviceConnected(@NonNull final BluetoothDevice device) {
mDeviceConnected = true;
runOnUiThread(() -> mConnectButton.setText(R.string.action_disconnect));
}
@Override
public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) {
runOnUiThread(() -> mConnectButton.setText(R.string.action_disconnecting));
}
@Override
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
mDeviceConnected = false;
mBleManager.close();
runOnUiThread(() -> {
mConnectButton.setText(R.string.action_connect);
mDeviceNameView.setText(getDefaultDeviceName());
});
}
@Override
public void onLinkLossOccurred(@NonNull final BluetoothDevice device) {
mDeviceConnected = false;
}
@Override
public void onServicesDiscovered(@NonNull final BluetoothDevice device, boolean optionalServicesFound) {
// this may notify user or show some views
}
@Override
public void onDeviceReady(@NonNull final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBondingRequired(@NonNull final BluetoothDevice device) {
showToast(R.string.bonding);
}
@Override
public void onBonded(@NonNull final BluetoothDevice device) {
showToast(R.string.bonded);
}
@Override
public void onBondingFailed(@NonNull final BluetoothDevice device) {
showToast(R.string.bonding_failed);
}
@Override
public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) {
DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode);
showToast(message + " (" + errorCode + ")");
}
@Override
public void onDeviceNotSupported(@NonNull final BluetoothDevice device) {
showToast(R.string.not_supported);
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param message a message to be shown
*/
protected void showToast(final String message) {
runOnUiThread(() -> Toast.makeText(BleProfileExpandableListActivity.this, message, Toast.LENGTH_SHORT).show());
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param messageResId an resource id of the message to be shown
*/
protected void showToast(final int messageResId) {
runOnUiThread(() -> Toast.makeText(BleProfileExpandableListActivity.this, messageResId, Toast.LENGTH_SHORT).show());
}
/**
* Returns <code>true</code> if the device is connected. Services may not have been discovered yet.
*/
protected boolean isDeviceConnected() {
return mDeviceConnected;
}
/**
* Returns the name of the device that the phone is currently connected to or was connected last time
*/
protected String getDeviceName() {
return mDeviceName;
}
/**
* Initializes the Bluetooth Low Energy manager. A manager is used to communicate with profile's services.
*
* @return the manager that was created
*/
protected abstract LoggableBleManager<? extends BleManagerCallbacks> initializeManager();
/**
* Restores the default UI before reconnecting
*/
protected abstract void setDefaultUI();
/**
* Returns the default device name resource id. The real device name is obtained when connecting to the device. This one is used when device has
* disconnected.
*
* @return the default device name resource id
*/
protected abstract int getDefaultDeviceName();
/**
* Returns the string resource id that will be shown in About box
*
* @return the about resource id
*/
protected abstract int getAboutTextId();
/**
* The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also:
* {@link #isChangingConfigurations()}.
*
* @return the required UUID or <code>null</code>
*/
protected abstract UUID getFilterUUID();
/**
* Shows the scanner fragment.
*
* @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their
* services
* @see #getFilterUUID()
*/
private void showDeviceScanningDialog(final UUID filter) {
runOnUiThread(() -> {
final ScannerFragment dialog = ScannerFragment.getInstance(filter);
dialog.show(getSupportFragmentManager(), "scan_fragment");
});
}
/**
* Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT.
*
* @return the logger session or <code>null</code>
*/
protected ILogSession getLogSession() {
return mLogSession;
}
private void ensureBLESupported() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show();
finish();
}
}
protected boolean isBLEEnabled() {
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
final BluetoothAdapter adapter = bluetoothManager.getAdapter();
return adapter != null && adapter.isEnabled();
}
protected void showBLEDialog() {
final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
}

View File

@@ -0,0 +1,603 @@
/*
* 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.profile;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.util.Log;
import android.widget.Toast;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.ble.utils.ILogger;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.Logger;
@SuppressWarnings("unused")
public abstract class BleProfileService extends Service implements BleManagerCallbacks {
@SuppressWarnings("unused")
private static final String TAG = "BleProfileService";
public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE";
public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED";
public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY";
public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE";
@Deprecated
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR";
/** The parameter passed when creating the service. Must contain the address of the sensor that we want to connect to */
public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS";
/** The key for the device name that is returned in {@link #BROADCAST_CONNECTION_STATE} with state {@link #STATE_CONNECTED}. */
public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME";
public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE";
public static final String EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI";
public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE";
public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE";
public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY";
public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY";
@Deprecated
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE";
public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE";
public static final int STATE_LINK_LOSS = -1;
public static final int STATE_DISCONNECTED = 0;
public static final int STATE_CONNECTED = 1;
public static final int STATE_CONNECTING = 2;
public static final int STATE_DISCONNECTING = 3;
private LoggableBleManager<BleManagerCallbacks> mBleManager;
private Handler mHandler;
protected boolean mBound;
private boolean mActivityIsChangingConfiguration;
private BluetoothDevice mBluetoothDevice;
private String mDeviceName;
private ILogSession mLogSession;
private final BroadcastReceiver mBluetoothStateBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
final ILogger logger = getBinder();
final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(state);
logger.log(Log.DEBUG, stateString);
switch (state) {
case BluetoothAdapter.STATE_ON:
onBluetoothEnabled();
break;
case BluetoothAdapter.STATE_TURNING_OFF:
case BluetoothAdapter.STATE_OFF:
onBluetoothDisabled();
break;
}
}
private String state2String(final int state) {
switch (state) {
case BluetoothAdapter.STATE_TURNING_ON:
return "TURNING ON";
case BluetoothAdapter.STATE_ON:
return "ON";
case BluetoothAdapter.STATE_TURNING_OFF:
return "TURNING OFF";
case BluetoothAdapter.STATE_OFF:
return "OFF";
default:
return "UNKNOWN (" + state + ")";
}
}
};
public class LocalBinder extends Binder implements ILogger {
/**
* Disconnects from the sensor.
*/
public final void disconnect() {
final int state = mBleManager.getConnectionState();
if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) {
mBleManager.close();
onDeviceDisconnected(mBluetoothDevice);
return;
}
mBleManager.disconnect().enqueue();
}
/**
* Sets whether the bound activity if changing configuration or not.
* If <code>false</code>, we will turn off battery level notifications in onUnbind(..) method below.
* @param changing true if the bound activity is finishing
*/
public void setActivityIsChangingConfiguration(final boolean changing) {
mActivityIsChangingConfiguration = changing;
}
/**
* Returns the device address
*
* @return device address
*/
public String getDeviceAddress() {
return mBluetoothDevice.getAddress();
}
/**
* Returns the device name
*
* @return the device name
*/
public String getDeviceName() {
return mDeviceName;
}
/**
* Returns the Bluetooth device
*
* @return the Bluetooth device
*/
public BluetoothDevice getBluetoothDevice() {
return mBluetoothDevice;
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
*
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
public boolean isConnected() {
return mBleManager.isConnected();
}
/**
* Returns the connection state of given device.
* @return the connection state, as in {@link BleManager#getConnectionState()}.
*/
public int getConnectionState() {
return mBleManager.getConnectionState();
}
/**
* Returns the log session that can be used to append log entries.
* The log session is created when the service is being created.
* The method returns <code>null</code> if the nRF Logger app was not installed.
*
* @return the log session
*/
public ILogSession getLogSession() {
return mLogSession;
}
@Override
public void log(final int level, @NonNull final String message) {
Logger.log(mLogSession, level, message);
}
@Override
public void log(final int level, final @StringRes int messageRes, final Object... params) {
Logger.log(mLogSession, level, messageRes, params);
}
}
/**
* Returns a handler that is created in onCreate().
* The handler may be used to postpone execution of some operations or to run them in UI thread.
*/
protected Handler getHandler() {
return mHandler;
}
/**
* Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity.
*
* @return the service binder
*/
protected LocalBinder getBinder() {
// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
return new LocalBinder();
}
@Override
public IBinder onBind(final Intent intent) {
mBound = true;
return getBinder();
}
@Override
public final void onRebind(final Intent intent) {
mBound = true;
if (!mActivityIsChangingConfiguration)
onRebind();
}
/**
* Called when the activity has rebound to the service after being recreated.
* This method is not called when the activity was killed to be recreated when the phone orientation changed
* if prior to being killed called {@link BleProfileService.LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true.
*/
protected void onRebind() {
// empty default implementation
}
@Override
public final boolean onUnbind(final Intent intent) {
mBound = false;
if (!mActivityIsChangingConfiguration)
onUnbind();
// We want the onRebind method be called if anything else binds to it again
return true;
}
/**
* Called when the activity has unbound from the service before being finished.
* This method is not called when the activity is killed to be recreated when the phone orientation changed.
*/
protected void onUnbind() {
// empty default implementation
}
@SuppressWarnings("unchecked")
@Override
public void onCreate() {
super.onCreate();
mHandler = new Handler();
// Initialize the manager
mBleManager = initializeManager();
mBleManager.setGattCallbacks(this);
// Register broadcast receivers
registerReceiver(mBluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
// Service has now been created
onServiceCreated();
// Call onBluetoothEnabled if Bluetooth enabled
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter.isEnabled()) {
onBluetoothEnabled();
}
}
/**
* Called when the service has been created, before the {@link #onBluetoothEnabled()} is called.
*/
protected void onServiceCreated() {
// empty default implementation
}
/**
* Initializes the Ble Manager responsible for connecting to a single device.
* @return a new BleManager object
*/
@SuppressWarnings("rawtypes")
protected abstract LoggableBleManager initializeManager();
/**
* This method returns whether autoConnect option should be used.
*
* @return true to use autoConnect feature, false (default) otherwise.
*/
protected boolean shouldAutoConnect() {
return false;
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS))
throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key");
final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI);
mLogSession = Logger.openSession(getApplicationContext(), logUri);
mDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME);
Logger.i(mLogSession, "Service started");
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
mBluetoothDevice = adapter.getRemoteDevice(deviceAddress);
mBleManager.setLogger(mLogSession);
onServiceStarted();
mBleManager.connect(mBluetoothDevice)
.useAutoConnect(shouldAutoConnect())
.retry(3, 100)
.enqueue();
return START_REDELIVER_INTENT;
}
/**
* Called when the service has been started. The device name and address are set.
* The BLE Manager will try to connect to the device after this method finishes.
*/
protected void onServiceStarted() {
// empty default implementation
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
// This method is called when user removed the app from Recents.
// By default, the service will be killed and recreated immediately after that.
// However, all managed devices will be lost and devices will be disconnected.
stopSelf();
}
@Override
public void onDestroy() {
super.onDestroy();
// Unregister broadcast receivers
unregisterReceiver(mBluetoothStateBroadcastReceiver);
// shutdown the manager
mBleManager.close();
Logger.i(mLogSession, "Service destroyed");
mBleManager = null;
mBluetoothDevice = null;
mDeviceName = null;
mLogSession = null;
mHandler = null;
}
/**
* Method called when Bluetooth Adapter has been disabled.
*/
protected void onBluetoothDisabled() {
// empty default implementation
}
/**
* This method is called when Bluetooth Adapter has been enabled and
* after the service was created if Bluetooth Adapter was enabled at that moment.
* This method could initialize all Bluetooth related features, for example open the GATT server.
*/
protected void onBluetoothEnabled() {
// empty default implementation
}
@Override
public void onDeviceConnecting(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceConnected(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_DEVICE_NAME, mDeviceName);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) {
// Notify user about changing the state to DISCONNECTING
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
/**
* This method should return false if the service needs to do some asynchronous work after if has disconnected from the device.
* In that case the {@link #stopService()} method must be called when done.
* @return true (default) to automatically stop the service when device is disconnected. False otherwise.
*/
protected boolean stopWhenDisconnected() {
return true;
}
@Override
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
// Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above
// Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be
// invoked ONLY when user requested disconnection (using Disconnect button). If the device
// disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead.
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
if (stopWhenDisconnected())
stopService();
}
protected void stopService() {
// user requested disconnection. We must stop the service
Logger.v(mLogSession, "Stopping service...");
stopSelf();
}
@Override
public void onLinkLossOccurred(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) {
final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true);
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceReady(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_DEVICE_READY);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceNotSupported(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false);
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
// no need for disconnecting, it will be disconnected by the manager automatically
}
@Override
public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_BATTERY_LEVEL, value);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBondingRequired(@NonNull final BluetoothDevice device) {
showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBonded(@NonNull final BluetoothDevice device) {
showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonded);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBondingFailed(@NonNull final BluetoothDevice device) {
showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding_failed);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) {
final Intent broadcast = new Intent(BROADCAST_ERROR);
broadcast.putExtra(EXTRA_DEVICE, mBluetoothDevice);
broadcast.putExtra(EXTRA_ERROR_MESSAGE, message);
broadcast.putExtra(EXTRA_ERROR_CODE, errorCode);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param messageResId
* an resource id of the message to be shown
*/
protected void showToast(final int messageResId) {
mHandler.post(() -> Toast.makeText(BleProfileService.this, messageResId, Toast.LENGTH_SHORT).show());
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param message
* a message to be shown
*/
protected void showToast(final String message) {
mHandler.post(() -> Toast.makeText(BleProfileService.this, message, Toast.LENGTH_SHORT).show());
}
/**
* Returns the log session that can be used to append log entries. The method returns <code>null</code> if the nRF Logger app was not installed. It is safe to use logger when
* {@link #onServiceStarted()} has been called.
*
* @return the log session
*/
protected ILogSession getLogSession() {
return mLogSession;
}
/**
* Returns the device address
*
* @return device address
*/
protected String getDeviceAddress() {
return mBluetoothDevice.getAddress();
}
/**
* Returns the Bluetooth device object
*
* @return bluetooth device
*/
protected BluetoothDevice getBluetoothDevice() {
return mBluetoothDevice;
}
/**
* Returns the device name
*
* @return the device name
*/
protected String getDeviceName() {
return mDeviceName;
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
*
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
protected boolean isConnected() {
return mBleManager != null && mBleManager.isConnected();
}
}

View File

@@ -0,0 +1,671 @@
/*
* 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.profile;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.LocalLogSession;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.AppHelpFragment;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;
import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
/**
* <p>
* The {@link BleProfileServiceReadyActivity} activity is designed to be the base class for profile activities that uses services in order to connect to the
* device. When user press CONNECT button a service is created and the activity binds to it. The service tries to connect to the service and notifies the
* activity using Local Broadcasts ({@link LocalBroadcastManager}). See {@link BleProfileService} for messages. If the device is not in range it will listen for
* it and connect when it become visible. The service exists until user will press DISCONNECT button.
* </p>
* <p>
* When user closes the activity (f.e. by pressing Back button) while being connected, the Service remains working. It's still connected to the device or still
* listens for it. When entering back to the activity, activity will to bind to the service and refresh UI.
* </p>
*/
@SuppressWarnings("unused")
public abstract class BleProfileServiceReadyActivity<E extends BleProfileService.LocalBinder> extends AppCompatActivity implements
ScannerFragment.OnDeviceSelectedListener, BleManagerCallbacks {
private static final String TAG = "BleProfileServiceReadyActivity";
private static final String SIS_DEVICE_NAME = "device_name";
private static final String SIS_DEVICE = "device";
private static final String LOG_URI = "log_uri";
protected static final int REQUEST_ENABLE_BT = 2;
private E mService;
private TextView mDeviceNameView;
private Button mConnectButton;
private ILogSession mLogSession;
private BluetoothDevice mBluetoothDevice;
private String mDeviceName;
private final BroadcastReceiver mCommonBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
// Check if the broadcast applies the connected device
if (!isBroadcastForThisDevice(intent))
return;
final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BleProfileService.EXTRA_DEVICE);
final String action = intent.getAction();
switch (action) {
case BleProfileService.BROADCAST_CONNECTION_STATE: {
final int state = intent.getIntExtra(BleProfileService.EXTRA_CONNECTION_STATE, BleProfileService.STATE_DISCONNECTED);
switch (state) {
case BleProfileService.STATE_CONNECTED: {
mDeviceName = intent.getStringExtra(BleProfileService.EXTRA_DEVICE_NAME);
onDeviceConnected(bluetoothDevice);
break;
}
case BleProfileService.STATE_DISCONNECTED: {
onDeviceDisconnected(bluetoothDevice);
mDeviceName = null;
break;
}
case BleProfileService.STATE_LINK_LOSS: {
onLinkLossOccurred(bluetoothDevice);
break;
}
case BleProfileService.STATE_CONNECTING: {
onDeviceConnecting(bluetoothDevice);
break;
}
case BleProfileService.STATE_DISCONNECTING: {
onDeviceDisconnecting(bluetoothDevice);
break;
}
default:
// there should be no other actions
break;
}
break;
}
case BleProfileService.BROADCAST_SERVICES_DISCOVERED: {
final boolean primaryService = intent.getBooleanExtra(BleProfileService.EXTRA_SERVICE_PRIMARY, false);
final boolean secondaryService = intent.getBooleanExtra(BleProfileService.EXTRA_SERVICE_SECONDARY, false);
if (primaryService) {
onServicesDiscovered(bluetoothDevice, secondaryService);
} else {
onDeviceNotSupported(bluetoothDevice);
}
break;
}
case BleProfileService.BROADCAST_DEVICE_READY: {
onDeviceReady(bluetoothDevice);
break;
}
case BleProfileService.BROADCAST_BOND_STATE: {
final int state = intent.getIntExtra(BleProfileService.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
switch (state) {
case BluetoothDevice.BOND_BONDING:
onBondingRequired(bluetoothDevice);
break;
case BluetoothDevice.BOND_BONDED:
onBonded(bluetoothDevice);
break;
}
break;
}
case BleProfileService.BROADCAST_ERROR: {
final String message = intent.getStringExtra(BleProfileService.EXTRA_ERROR_MESSAGE);
final int errorCode = intent.getIntExtra(BleProfileService.EXTRA_ERROR_CODE, 0);
onError(bluetoothDevice, message, errorCode);
break;
}
}
}
};
private ServiceConnection mServiceConnection = new ServiceConnection() {
@SuppressWarnings("unchecked")
@Override
public void onServiceConnected(final ComponentName name, final IBinder service) {
final E bleService = mService = (E) service;
mBluetoothDevice = bleService.getBluetoothDevice();
mLogSession = mService.getLogSession();
Logger.d(mLogSession, "Activity bound to the service");
onServiceBound(bleService);
// Update UI
mDeviceName = bleService.getDeviceName();
mDeviceNameView.setText(mDeviceName);
mConnectButton.setText(R.string.action_disconnect);
// And notify user if device is connected
if (bleService.isConnected()) {
onDeviceConnected(mBluetoothDevice);
} else {
// If the device is not connected it means that either it is still connecting,
// or the link was lost and service is trying to connect to it (autoConnect=true).
onDeviceConnecting(mBluetoothDevice);
}
}
@Override
public void onServiceDisconnected(final ComponentName name) {
// Note: this method is called only when the service is killed by the system,
// not when it stops itself or is stopped by the activity.
// It will be called only when there is critically low memory, in practice never
// when the activity is in foreground.
Logger.d(mLogSession, "Activity disconnected from the service");
mDeviceNameView.setText(getDefaultDeviceName());
mConnectButton.setText(R.string.action_connect);
mService = null;
mDeviceName = null;
mBluetoothDevice = null;
mLogSession = null;
onServiceUnbound();
}
};
@Override
protected final void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ensureBLESupported();
if (!isBLEEnabled()) {
showBLEDialog();
}
// Restore the old log session
if (savedInstanceState != null) {
final Uri logUri = savedInstanceState.getParcelable(LOG_URI);
mLogSession = Logger.openSession(getApplicationContext(), logUri);
}
// In onInitialize method a final class may register local broadcast receivers that will listen for events from the service
onInitialize(savedInstanceState);
// The onCreateView class should... create the view
onCreateView(savedInstanceState);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
// Common nRF Toolbox view references are obtained here
setUpView();
// View is ready to be used
onViewCreated(savedInstanceState);
LocalBroadcastManager.getInstance(this).registerReceiver(mCommonBroadcastReceiver, makeIntentFilter());
}
@Override
protected void onStart() {
super.onStart();
/*
* If the service has not been started before, the following lines will not start it.
* However, if it's running, the Activity will bind to it and notified via mServiceConnection.
*/
final Intent service = new Intent(this, getServiceClass());
// We pass 0 as a flag so the service will not be created if not exists.
bindService(service, mServiceConnection, 0);
/*
* When user exited the UARTActivity while being connected, the log session is kept in
* the service. We may not get it before binding to it so in this case this event will
* not be logged (mLogSession is null until onServiceConnected(..) is called).
* It will, however, be logged after the orientation changes.
*/
}
@Override
protected void onStop() {
super.onStop();
try {
// We don't want to perform some operations (e.g. disable Battery Level notifications)
// in the service if we are just rotating the screen. However, when the activity will
// disappear, we may want to disable some device features to reduce the battery
// consumption.
if (mService != null)
mService.setActivityIsChangingConfiguration(isChangingConfigurations());
unbindService(mServiceConnection);
mService = null;
Logger.d(mLogSession, "Activity unbound from the service");
onServiceUnbound();
mDeviceName = null;
mBluetoothDevice = null;
mLogSession = null;
} catch (final IllegalArgumentException e) {
// do nothing, we were not connected to the sensor
}
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mCommonBroadcastReceiver);
}
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BleProfileService.BROADCAST_CONNECTION_STATE);
intentFilter.addAction(BleProfileService.BROADCAST_SERVICES_DISCOVERED);
intentFilter.addAction(BleProfileService.BROADCAST_DEVICE_READY);
intentFilter.addAction(BleProfileService.BROADCAST_BOND_STATE);
intentFilter.addAction(BleProfileService.BROADCAST_ERROR);
return intentFilter;
}
/**
* Called when activity binds to the service. The parameter is the object returned in {@link Service#onBind(Intent)} method in your service. The method is
* called when device gets connected or is created while sensor was connected before. You may use the binder as a sensor interface.
*/
protected abstract void onServiceBound(E binder);
/**
* Called when activity unbinds from the service. You may no longer use this binder because the sensor was disconnected. This method is also called when you
* leave the activity being connected to the sensor in the background.
*/
protected abstract void onServiceUnbound();
/**
* Returns the service class for sensor communication. The service class must derive from {@link BleProfileService} in order to operate with this class.
*
* @return the service class
*/
protected abstract Class<? extends BleProfileService> getServiceClass();
/**
* Returns the service interface that may be used to communicate with the sensor. This will return <code>null</code> if the device is disconnected from the
* sensor.
*
* @return the service binder or <code>null</code>
*/
protected E getService() {
return mService;
}
/**
* You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created.
*/
protected void onInitialize(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}.
* Use to obtain references to views. Connect/Disconnect button, the device name view are manager automatically.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
protected abstract void onCreateView(final Bundle savedInstanceState);
/**
* Called after the view has been created.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
protected void onViewCreated(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called after the view and the toolbar has been created.
*/
protected final void setUpView() {
// set GUI
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mConnectButton = findViewById(R.id.action_connect);
mDeviceNameView = findViewById(R.id.device_name);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(SIS_DEVICE_NAME, mDeviceName);
outState.putParcelable(SIS_DEVICE, mBluetoothDevice);
if (mLogSession != null)
outState.putParcelable(LOG_URI, mLogSession.getSessionUri());
}
@Override
protected void onRestoreInstanceState(final @NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mDeviceName = savedInstanceState.getString(SIS_DEVICE_NAME);
mBluetoothDevice = savedInstanceState.getParcelable(SIS_DEVICE);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.help, menu);
return true;
}
/**
* Use this method to handle menu actions other than home and about.
*
* @param itemId the menu item id
* @return <code>true</code> if action has been handled
*/
protected boolean onOptionsItemSelected(final int itemId) {
// Overwrite when using menu other than R.menu.help
return false;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
onBackPressed();
break;
case R.id.action_about:
final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId());
fragment.show(getSupportFragmentManager(), "help_fragment");
break;
default:
return onOptionsItemSelected(id);
}
return true;
}
/**
* Called when user press CONNECT or DISCONNECT button. See layout files -> onClick attribute.
*/
public void onConnectClicked(final View view) {
if (isBLEEnabled()) {
if (mService == null) {
setDefaultUI();
showDeviceScanningDialog(getFilterUUID());
} else {
mService.disconnect();
}
} else {
showBLEDialog();
}
}
/**
* Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used.
*
* @return the title resource id
*/
protected int getLoggerProfileTitle() {
return 0;
}
/**
* This method may return the local log content provider authority if local log sessions are supported.
*
* @return local log session content provider URI
*/
protected Uri getLocalAuthorityLogger() {
return null;
}
@Override
public void onDeviceSelected(final BluetoothDevice device, final String name) {
final int titleId = getLoggerProfileTitle();
if (titleId > 0) {
mLogSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name);
// If nRF Logger is not installed we may want to use local logger
if (mLogSession == null && getLocalAuthorityLogger() != null) {
mLogSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name);
}
}
mBluetoothDevice = device;
mDeviceName = name;
// The device may not be in the range but the service will try to connect to it if it reach it
Logger.d(mLogSession, "Creating service...");
final Intent service = new Intent(this, getServiceClass());
service.putExtra(BleProfileService.EXTRA_DEVICE_ADDRESS, device.getAddress());
service.putExtra(BleProfileService.EXTRA_DEVICE_NAME, name);
if (mLogSession != null)
service.putExtra(BleProfileService.EXTRA_LOG_URI, mLogSession.getSessionUri());
startService(service);
Logger.d(mLogSession, "Binding to the service...");
bindService(service, mServiceConnection, 0);
}
@Override
public void onDialogCanceled() {
// do nothing
}
@Override
public void onDeviceConnecting(final BluetoothDevice device) {
mDeviceNameView.setText(mDeviceName != null ? mDeviceName : getString(R.string.not_available));
mConnectButton.setText(R.string.action_connecting);
}
@Override
public void onDeviceConnected(final BluetoothDevice device) {
mDeviceNameView.setText(mDeviceName);
mConnectButton.setText(R.string.action_disconnect);
}
@Override
public void onDeviceDisconnecting(final BluetoothDevice device) {
mConnectButton.setText(R.string.action_disconnecting);
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
mConnectButton.setText(R.string.action_connect);
mDeviceNameView.setText(getDefaultDeviceName());
try {
Logger.d(mLogSession, "Unbinding from the service...");
unbindService(mServiceConnection);
mService = null;
Logger.d(mLogSession, "Activity unbound from the service");
onServiceUnbound();
mDeviceName = null;
mBluetoothDevice = null;
mLogSession = null;
} catch (final IllegalArgumentException e) {
// do nothing. This should never happen but does...
}
}
@Override
public void onLinkLossOccurred(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
// empty default implementation
}
@Override
public void onDeviceReady(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBondingRequired(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBonded(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBondingFailed(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onError(final BluetoothDevice device, final String message, final int errorCode) {
DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode);
showToast(message + " (" + errorCode + ")");
}
@Override
public void onDeviceNotSupported(final BluetoothDevice device) {
showToast(R.string.not_supported);
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param message a message to be shown
*/
protected void showToast(final String message) {
runOnUiThread(() -> Toast.makeText(BleProfileServiceReadyActivity.this, message, Toast.LENGTH_LONG).show());
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param messageResId an resource id of the message to be shown
*/
protected void showToast(final int messageResId) {
runOnUiThread(() -> Toast.makeText(BleProfileServiceReadyActivity.this, messageResId, Toast.LENGTH_SHORT).show());
}
/**
* Returns <code>true</code> if the device is connected. Services may not have been discovered yet.
*/
protected boolean isDeviceConnected() {
return mService != null && mService.isConnected();
}
/**
* Returns the name of the device that the phone is currently connected to or was connected last time
*/
protected String getDeviceName() {
return mDeviceName;
}
/**
* Restores the default UI before reconnecting
*/
protected abstract void setDefaultUI();
/**
* Returns the default device name resource id. The real device name is obtained when connecting to the device. This one is used when device has
* disconnected.
*
* @return the default device name resource id
*/
protected abstract int getDefaultDeviceName();
/**
* Returns the string resource id that will be shown in About box
*
* @return the about resource id
*/
protected abstract int getAboutTextId();
/**
* The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also:
* {@link #isChangingConfigurations()}.
*
* @return the required UUID or <code>null</code>
*/
protected abstract UUID getFilterUUID();
/**
* Checks the {@link BleProfileService#EXTRA_DEVICE} in the given intent and compares it with the connected BluetoothDevice object.
* @param intent intent received via a broadcast from the service
* @return true if the data in the intent apply to the connected device, false otherwise
*/
protected boolean isBroadcastForThisDevice(final Intent intent) {
final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BleProfileService.EXTRA_DEVICE);
return mBluetoothDevice != null && mBluetoothDevice.equals(bluetoothDevice);
}
/**
* Shows the scanner fragment.
*
* @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their
* services
* @see #getFilterUUID()
*/
private void showDeviceScanningDialog(final UUID filter) {
final ScannerFragment dialog = ScannerFragment.getInstance(filter);
dialog.show(getSupportFragmentManager(), "scan_fragment");
}
/**
* Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT.
*
* @return the logger session or <code>null</code>
*/
protected ILogSession getLogSession() {
return mLogSession;
}
private void ensureBLESupported() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show();
finish();
}
}
protected boolean isBLEEnabled() {
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
final BluetoothAdapter adapter = bluetoothManager.getAdapter();
return adapter != null && adapter.isEnabled();
}
protected void showBLEDialog() {
final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
}

View File

@@ -0,0 +1,48 @@
package no.nordicsemi.android.nrftoolbox.profile;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.log.Logger;
/**
* The manager that logs to nRF Logger. If nRF Logger is not installed, logs are ignored.
*
* @param <T> the callbacks class.
*/
public abstract class LoggableBleManager<T extends BleManagerCallbacks> extends BleManager<T> {
private ILogSession mLogSession;
/**
* The manager constructor.
* <p>
* After constructing the manager, the callbacks object must be set with
* {@link #setGattCallbacks(BleManagerCallbacks)}.
*
* @param context the context.
*/
public LoggableBleManager(@NonNull final Context context) {
super(context);
}
/**
* Sets the log session to log into.
*
* @param session nRF Logger log session to log inti, or null, if nRF Logger is not installed.
*/
public void setLogger(@Nullable final ILogSession session) {
mLogSession = session;
}
@Override
public void log(final int priority, @NonNull final String message) {
Logger.log(mLogSession, LogContract.Log.Level.fromPriority(priority), message);
Log.println(priority, "BleManager", message);
}
}

View File

@@ -0,0 +1,621 @@
/*
* 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.profile.multiconnect;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.ble.utils.ILogger;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
public abstract class BleMulticonnectProfileService extends Service implements BleManagerCallbacks {
@SuppressWarnings("unused")
private static final String TAG = "BleMultiProfileService";
public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE";
public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED";
public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY";
public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE";
@Deprecated
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR";
public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE";
public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE";
public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE";
public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY";
public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY";
@Deprecated
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE";
public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE";
public static final int STATE_LINK_LOSS = -1;
public static final int STATE_DISCONNECTED = 0;
public static final int STATE_CONNECTED = 1;
public static final int STATE_CONNECTING = 2;
public static final int STATE_DISCONNECTING = 3;
private HashMap<BluetoothDevice, LoggableBleManager<BleManagerCallbacks>> mBleManagers;
private List<BluetoothDevice> mManagedDevices;
private Handler mHandler;
protected boolean mBound;
private boolean mActivityIsChangingConfiguration;
private final BroadcastReceiver mBluetoothStateBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
final int previousState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);
switch (state) {
case BluetoothAdapter.STATE_ON:
// On older phones (tested on Nexus 4 with Android 5.0.1) the Bluetooth requires some time
// after it has been enabled before some operations can start. Starting the GATT server here
// without a delay is very likely to cause a DeadObjectException from BluetoothManager#openGattServer(...).
mHandler.postDelayed(() -> onBluetoothEnabled(), 600);
break;
case BluetoothAdapter.STATE_TURNING_OFF:
case BluetoothAdapter.STATE_OFF:
if (previousState != BluetoothAdapter.STATE_TURNING_OFF && previousState != BluetoothAdapter.STATE_OFF)
onBluetoothDisabled();
break;
}
}
};
public class LocalBinder extends Binder implements ILogger, IDeviceLogger {
/**
* Returns an unmodifiable list of devices managed by the service.
* The returned devices do not need to be connected at tha moment. Each of them was however created
* using {@link #connect(BluetoothDevice)} method so they might have been connected before and disconnected.
* @return unmodifiable list of devices managed by the service
*/
public final List<BluetoothDevice> getManagedDevices() {
return Collections.unmodifiableList(mManagedDevices);
}
/**
* Connects to the given device. If the device is already connected this method does nothing.
* @param device target Bluetooth device
*/
public void connect(final BluetoothDevice device) {
connect(device, null);
}
/**
* Adds the given device to managed and stars connecting to it. If the device is already connected this method does nothing.
* @param device target Bluetooth device
* @param session log session that has to be used by the device
*/
@SuppressWarnings("unchecked")
public void connect(final BluetoothDevice device, final ILogSession session) {
// If a device is in managed devices it means that it's already connected, or was connected
// using autoConnect and the link was lost but Android is already trying to connect to it.
if (mManagedDevices.contains(device))
return;
mManagedDevices.add(device);
LoggableBleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
if (manager != null) {
if (session != null)
manager.setLogger(session);
manager.connect(device).enqueue();
} else {
mBleManagers.put(device, manager = initializeManager());
manager.setGattCallbacks(BleMulticonnectProfileService.this);
manager.setLogger(session);
manager.connect(device)
.fail((d, status) -> {
mManagedDevices.remove(device);
mBleManagers.remove(device);
})
.enqueue(10000);
}
}
/**
* Disconnects the given device and removes the associated BleManager object.
* If the list of BleManagers is empty while the last activity unbinds from the service,
* the service will stop itself.
* @param device target device to disconnect and forget
*/
public void disconnect(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
if (manager != null && manager.isConnected()) {
manager.disconnect().enqueue();
}
mManagedDevices.remove(device);
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
* @param device the target device
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
public final boolean isConnected(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
return manager != null && manager.isConnected();
}
/**
* Returns <code>true</code> if the device has finished initializing.
* @param device the target device
* @return <code>true</code> if device is connected to the sensor and has finished
* initializing. False otherwise.
*/
public final boolean isReady(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
return manager != null && manager.isReady();
}
/**
* Returns the connection state of given device.
* @param device the target device
* @return the connection state, as in {@link BleManager#getConnectionState()}.
*/
public final int getConnectionState(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
return manager != null ? manager.getConnectionState() : BluetoothGatt.STATE_DISCONNECTED;
}
/**
* Returns the last received battery level value.
* @param device the device of which battery level should be returned
* @return battery value or -1 if no value was received or Battery Level characteristic was not found
* @deprecated Keep battery value in your manager instead.
*/
@Deprecated
public int getBatteryValue(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
return manager.getBatteryValue();
}
/**
* Sets whether the bound activity if changing configuration or not.
* If <code>false</code>, we will turn off battery level notifications in onUnbind(..) method below.
* @param changing true if the bound activity is finishing
*/
public final void setActivityIsChangingConfiguration(final boolean changing) {
mActivityIsChangingConfiguration = changing;
}
@Override
public void log(final BluetoothDevice device, final int level, final String message) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
if (manager != null)
manager.log(level, message);
}
@Override
public void log(final BluetoothDevice device, final int level, @StringRes final int messageRes, final Object... params) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
if (manager != null)
manager.log(level, messageRes, params);
}
@Override
public void log(final int level, @NonNull final String message) {
for (final BleManager<BleManagerCallbacks> manager : mBleManagers.values())
manager.log(level, message);
}
@Override
public void log(final int level, @StringRes final int messageRes, final Object... params) {
for (final BleManager<BleManagerCallbacks> manager : mBleManagers.values())
manager.log(level, messageRes, params);
}
}
/**
* Returns a handler that is created in onCreate().
* The handler may be used to postpone execution of some operations or to run them in UI thread.
*/
protected Handler getHandler() {
return mHandler;
}
/**
* Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity.
*
* @return the service binder
*/
protected LocalBinder getBinder() {
// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
return new LocalBinder();
}
@Override
public IBinder onBind(final Intent intent) {
mBound = true;
return getBinder();
}
@Override
public final void onRebind(final Intent intent) {
mBound = true;
if (!mActivityIsChangingConfiguration) {
onRebind();
}
}
/**
* Called when the activity has rebound to the service after being recreated.
* This method is not called when the activity was killed to be recreated when the phone orientation changed
* if prior to being killed called {@link LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true.
*/
protected void onRebind() {
// empty default implementation
}
@Override
public final boolean onUnbind(final Intent intent) {
mBound = false;
if (!mActivityIsChangingConfiguration) {
if (!mManagedDevices.isEmpty()) {
onUnbind();
} else {
// The last activity has disconnected from the service and there are no devices to manage. The service may be stopped.
stopSelf();
}
}
// We want the onRebind method be called if anything else binds to it again
return true;
}
/**
* Called when the activity has unbound from the service before being finished.
* This method is not called when the activity is killed to be recreated when the phone orientation changed.
*/
protected void onUnbind() {
// empty default implementation
}
@Override
public void onCreate() {
super.onCreate();
mHandler = new Handler();
// Initialize the map of BLE managers
mBleManagers = new HashMap<>();
mManagedDevices = new ArrayList<>();
// Register broadcast receivers
registerReceiver(mBluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
// Service has now been created
onServiceCreated();
// Call onBluetoothEnabled if Bluetooth enabled
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter.isEnabled()) {
onBluetoothEnabled();
}
}
/**
* Called when the service has been created, before the {@link #onBluetoothEnabled()} is called.
*/
protected void onServiceCreated() {
// empty default implementation
}
/**
* Initializes the Ble Manager responsible for connecting to a single device.
* @return a new BleManager object
*/
@SuppressWarnings("rawtypes")
protected abstract LoggableBleManager initializeManager();
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
onServiceStarted();
// The service does not save addresses of managed devices.
// A bound activity will be required to add connections again.
return START_NOT_STICKY;
}
/**
* Called when the service has been started.
*/
protected void onServiceStarted() {
// empty default implementation
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
// This method is called when user removed the app from Recents.
// By default, the service will be killed and recreated immediately after that.
// However, all managed devices will be lost and devices will be disconnected.
stopSelf();
}
@Override
public void onDestroy() {
super.onDestroy();
onServiceStopped();
mHandler = null;
}
/**
* Called when the service has been stopped.
*/
protected void onServiceStopped() {
// Unregister broadcast receivers
unregisterReceiver(mBluetoothStateBroadcastReceiver);
// The managers map may not be empty if the service was killed by the system
for (final BleManager<BleManagerCallbacks> manager : mBleManagers.values()) {
// Service is being destroyed, no need to disconnect manually.
manager.close();
manager.log(Log.INFO, "Service destroyed");
}
mBleManagers.clear();
mManagedDevices.clear();
mBleManagers = null;
mManagedDevices = null;
}
/**
* Method called when Bluetooth Adapter has been disabled.
*/
protected void onBluetoothDisabled() {
// do nothing, BleManagers have their own Bluetooth State broadcast received and will close themselves
}
/**
* This method is called when Bluetooth Adapter has been enabled. It is also called
* after the service was created if Bluetooth Adapter was enabled at that moment.
* This method could initialize all Bluetooth related features, for example open the GATT server.
* Make sure you call <code>super.onBluetoothEnabled()</code> at this methods reconnects to
* devices that were connected before the Bluetooth was turned off.
*/
protected void onBluetoothEnabled() {
for (final BluetoothDevice device : mManagedDevices) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
if (!manager.isConnected())
manager.connect(device).enqueue();
}
}
@Override
public void onDeviceConnecting(final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING);
LocalBroadcastManager.getInstance(BleMulticonnectProfileService.this).sendBroadcast(broadcast);
}
@Override
public void onDeviceConnected(final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceDisconnecting(final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
// Note: if BleManager#shouldAutoConnect() for this device returned true, this callback will be
// invoked ONLY when user requested disconnection (using Disconnect button). If the device
// disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead.
// We no longer want to keep the device in the service
mManagedDevices.remove(device);
// The BleManager is not removed from the HashMap in order to keep the device's log session.
// mBleManagers.remove(device);
// Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
// When user disconnected the last device while the activity was not bound the service can be stopped
if (!mBound && mManagedDevices.isEmpty()) {
stopSelf();
}
}
@Override
public void onLinkLossOccurred(final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true);
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceReady(final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_DEVICE_READY);
broadcast.putExtra(EXTRA_DEVICE, device);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceNotSupported(final BluetoothDevice device) {
// We don't like this device, remove it from both collections
mManagedDevices.remove(device);
mBleManagers.remove(device);
final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false);
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
// no need for disconnecting, it will be disconnected by the manager automatically
}
@Override
public void onBatteryValueReceived(final BluetoothDevice device, final int value) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_BATTERY_LEVEL, value);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBondingRequired(final BluetoothDevice device) {
showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBonded(final BluetoothDevice device) {
showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonded);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBondingFailed(final BluetoothDevice device) {
showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding_failed);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onError(final BluetoothDevice device, final String message, final int errorCode) {
final Intent broadcast = new Intent(BROADCAST_ERROR);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_ERROR_MESSAGE, message);
broadcast.putExtra(EXTRA_ERROR_CODE, errorCode);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param messageResId
* an resource id of the message to be shown
*/
protected void showToast(final int messageResId) {
mHandler.post(() -> Toast.makeText(BleMulticonnectProfileService.this, messageResId, Toast.LENGTH_SHORT).show());
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param message
* a message to be shown
*/
protected void showToast(final String message) {
mHandler.post(() -> Toast.makeText(BleMulticonnectProfileService.this, message, Toast.LENGTH_SHORT).show());
}
/**
* Returns the {@link BleManager} object associated with given device, or null if such has not been created.
* To create a BleManager call the {@link LocalBinder#connect(BluetoothDevice)} method must be called.
* @param device the target device
* @return the BleManager or null
*/
protected BleManager<? extends BleManagerCallbacks> getBleManager(final BluetoothDevice device) {
return mBleManagers.get(device);
}
/**
* Returns unmodifiable list of all managed devices. They don't have to be connected at the moment.
* @return list of managed devices
*/
protected List<BluetoothDevice> getManagedDevices() {
return Collections.unmodifiableList(mManagedDevices);
}
/**
* Returns a list of those managed devices that are connected at the moment.
* @return list of connected devices
*/
protected List<BluetoothDevice> getConnectedDevices() {
final List<BluetoothDevice> list = new ArrayList<>();
for (BluetoothDevice device : mManagedDevices) {
if (mBleManagers.get(device).isConnected())
list.add(device);
}
return Collections.unmodifiableList(list);
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
* @param device the target device
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
protected boolean isConnected(final BluetoothDevice device) {
final BleManager<BleManagerCallbacks> manager = mBleManagers.get(device);
return manager != null && manager.isConnected();
}
}

View File

@@ -0,0 +1,549 @@
/*
* 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.profile.multiconnect;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.LocalLogSession;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.nrftoolbox.AppHelpFragment;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;
import no.nordicsemi.android.nrftoolbox.utility.DebugLogger;
/**
* <p>
* The {@link BleMulticonnectProfileServiceReadyActivity} activity is designed to be the base class for profile activities that uses services in order to connect
* more than one device at the same time. A service extending {@link BleMulticonnectProfileService} is created when the activity is created, and the activity binds to it.
* The service returns a binder that may be used to connect, disconnect or manage devices, and notifies the
* activity using Local Broadcasts ({@link LocalBroadcastManager}). See {@link BleMulticonnectProfileService} for messages. If the device is not in range it will listen for
* it and connect when it become visible. The service exists until all managed devices have been disconnected and unmanaged and the last activity unbinds from it.
* </p>
* <p>
* When user closes the activity (e.g. by pressing Back button) while being connected, the Service remains working. It's remains connected to the devices or still
* listens for updates from them. When entering back to the activity, activity will to bind to the service and refresh UI.
* </p>
*/
public abstract class BleMulticonnectProfileServiceReadyActivity<E extends BleMulticonnectProfileService.LocalBinder> extends AppCompatActivity implements
ScannerFragment.OnDeviceSelectedListener, BleManagerCallbacks {
private static final String TAG = "BleMulticonnectProfileServiceReadyActivity";
protected static final int REQUEST_ENABLE_BT = 2;
private E mService;
private List<BluetoothDevice> mManagedDevices;
private final BroadcastReceiver mCommonBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BleMulticonnectProfileService.EXTRA_DEVICE);
final String action = intent.getAction();
switch (action) {
case BleMulticonnectProfileService.BROADCAST_CONNECTION_STATE: {
final int state = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_CONNECTION_STATE, BleMulticonnectProfileService.STATE_DISCONNECTED);
switch (state) {
case BleMulticonnectProfileService.STATE_CONNECTED: {
onDeviceConnected(bluetoothDevice);
break;
}
case BleMulticonnectProfileService.STATE_DISCONNECTED: {
onDeviceDisconnected(bluetoothDevice);
break;
}
case BleMulticonnectProfileService.STATE_LINK_LOSS: {
onLinkLossOccurred(bluetoothDevice);
break;
}
case BleMulticonnectProfileService.STATE_CONNECTING: {
onDeviceConnecting(bluetoothDevice);
break;
}
case BleMulticonnectProfileService.STATE_DISCONNECTING: {
onDeviceDisconnecting(bluetoothDevice);
break;
}
default:
// there should be no other actions
break;
}
break;
}
case BleMulticonnectProfileService.BROADCAST_SERVICES_DISCOVERED: {
final boolean primaryService = intent.getBooleanExtra(BleMulticonnectProfileService.EXTRA_SERVICE_PRIMARY, false);
final boolean secondaryService = intent.getBooleanExtra(BleMulticonnectProfileService.EXTRA_SERVICE_SECONDARY, false);
if (primaryService) {
onServicesDiscovered(bluetoothDevice, secondaryService);
} else {
onDeviceNotSupported(bluetoothDevice);
}
break;
}
case BleMulticonnectProfileService.BROADCAST_DEVICE_READY: {
onDeviceReady(bluetoothDevice);
break;
}
case BleMulticonnectProfileService.BROADCAST_BOND_STATE: {
final int state = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
switch (state) {
case BluetoothDevice.BOND_BONDING:
onBondingRequired(bluetoothDevice);
break;
case BluetoothDevice.BOND_BONDED:
onBonded(bluetoothDevice);
break;
}
break;
}
case BleMulticonnectProfileService.BROADCAST_BATTERY_LEVEL: {
final int value = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_BATTERY_LEVEL, -1);
if (value > 0)
onBatteryValueReceived(bluetoothDevice, value);
break;
}
case BleMulticonnectProfileService.BROADCAST_ERROR: {
final String message = intent.getStringExtra(BleMulticonnectProfileService.EXTRA_ERROR_MESSAGE);
final int errorCode = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_ERROR_CODE, 0);
onError(bluetoothDevice, message, errorCode);
break;
}
}
}
};
private ServiceConnection mServiceConnection = new ServiceConnection() {
@SuppressWarnings("unchecked")
@Override
public void onServiceConnected(final ComponentName name, final IBinder service) {
final E bleService = mService = (E) service;
bleService.log(Log.DEBUG, "Activity bound to the service");
mManagedDevices.addAll(bleService.getManagedDevices());
onServiceBound(bleService);
// and notify user if device is connected
for (final BluetoothDevice device : mManagedDevices) {
if (bleService.isConnected(device))
onDeviceConnected(device);
}
}
@Override
public void onServiceDisconnected(final ComponentName name) {
mService = null;
onServiceUnbound();
}
};
@Override
protected final void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mManagedDevices = new ArrayList<>();
ensureBLESupported();
if (!isBLEEnabled()) {
showBLEDialog();
}
// In onInitialize method a final class may register local broadcast receivers that will listen for events from the service
onInitialize(savedInstanceState);
// The onCreateView class should... create the view
onCreateView(savedInstanceState);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
// Common nRF Toolbox view references are obtained here
setUpView();
// View is ready to be used
onViewCreated(savedInstanceState);
LocalBroadcastManager.getInstance(this).registerReceiver(mCommonBroadcastReceiver, makeIntentFilter());
}
@Override
protected void onStart() {
super.onStart();
/*
* In comparison to BleProfileServiceReadyActivity this activity always starts the service when started.
* Connecting to a device is done by calling mService.connect(BluetoothDevice) method, not startService(...) like there.
* The service will stop itself when all devices it manages were disconnected and unmanaged and the last activity unbinds from it.
*/
final Intent service = new Intent(this, getServiceClass());
startService(service);
bindService(service, mServiceConnection, 0);
}
@Override
protected void onStop() {
super.onStop();
if (mService != null) {
// We don't want to perform some operations (e.g. disable Battery Level notifications) in the service if we are just rotating the screen.
// However, when the activity will disappear, we may want to disable some device features to reduce the battery consumption.
mService.setActivityIsChangingConfiguration(isChangingConfigurations());
// Log it here as there is no callback when the service gets unbound
// and the mService will not be available later (the activity doesn't keep log sessions)
mService.log(Log.DEBUG, "Activity unbound from the service");
}
unbindService(mServiceConnection);
mService = null;
onServiceUnbound();
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mCommonBroadcastReceiver);
}
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_CONNECTION_STATE);
intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_SERVICES_DISCOVERED);
intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_DEVICE_READY);
intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_BOND_STATE);
intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_BATTERY_LEVEL);
intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_ERROR);
return intentFilter;
}
/**
* Called when activity binds to the service. The parameter is the object returned in {@link Service#onBind(Intent)} method in your service.
* It is safe to obtain managed devices now.
*/
protected abstract void onServiceBound(E binder);
/**
* Called when activity unbinds from the service. You may no longer use this binder methods.
*/
protected abstract void onServiceUnbound();
/**
* Returns the service class for sensor communication. The service class must derive from {@link BleMulticonnectProfileService} in order to operate with this class.
*
* @return the service class
*/
protected abstract Class<? extends BleMulticonnectProfileService> getServiceClass();
/**
* Returns the service interface that may be used to communicate with the sensor. This will return <code>null</code> if the device is disconnected from the
* sensor.
*
* @return the service binder or <code>null</code>
*/
protected E getService() {
return mService;
}
/**
* You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created.
*/
protected void onInitialize(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}.
* Use to obtain references to views. Connect/Disconnect button and the device name view are manager automatically.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
protected abstract void onCreateView(final Bundle savedInstanceState);
/**
* Called after the view has been created.
*
* @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}.
* Note: <b>Otherwise it is null</b>.
*/
protected void onViewCreated(final Bundle savedInstanceState) {
// empty default implementation
}
/**
* Called after the view and the toolbar has been created.
*/
protected final void setUpView() {
// set GUI
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.help, menu);
return true;
}
/**
* Use this method to handle menu actions other than home and about.
*
* @param itemId the menu item id
* @return <code>true</code> if action has been handled
*/
protected boolean onOptionsItemSelected(final int itemId) {
// Overwrite when using menu other than R.menu.help
return false;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
onBackPressed();
break;
case R.id.action_about:
final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId());
fragment.show(getSupportFragmentManager(), "help_fragment");
break;
default:
return onOptionsItemSelected(id);
}
return true;
}
/**
* Called when user press ADD DEVICE button. See layout files -> onClick attribute.
*/
public void onAddDeviceClicked(final View view) {
if (isBLEEnabled()) {
showDeviceScanningDialog(getFilterUUID());
} else {
showBLEDialog();
}
}
/**
* Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used.
*
* @return the title resource id
*/
protected int getLoggerProfileTitle() {
return 0;
}
/**
* This method may return the local log content provider authority if local log sessions are supported.
*
* @return local log session content provider URI
*/
protected Uri getLocalAuthorityLogger() {
return null;
}
@Override
public void onDeviceSelected(final BluetoothDevice device, final String name) {
final int titleId = getLoggerProfileTitle();
ILogSession logSession = null;
if (titleId > 0) {
logSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name);
// If nRF Logger is not installed we may want to use local logger
if (logSession == null && getLocalAuthorityLogger() != null) {
logSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name);
}
}
mService.connect(device, logSession);
}
@Override
public void onDialogCanceled() {
// do nothing
}
@Override
public void onDeviceConnecting(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onDeviceDisconnecting(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onLinkLossOccurred(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
// empty default implementation
}
@Override
public void onDeviceReady(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBondingRequired(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBonded(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onBondingFailed(final BluetoothDevice device) {
// empty default implementation
}
@Override
public void onDeviceNotSupported(final BluetoothDevice device) {
showToast(R.string.not_supported);
}
@Override
public final boolean shouldEnableBatteryLevelNotifications(final BluetoothDevice device) {
// This method will never be called.
// Please see BleMulticonnectProfileService#shouldEnableBatteryLevelNotifications(BluetoothDevice) instead.
throw new UnsupportedOperationException("This method should not be called");
}
@Override
public void onBatteryValueReceived(final BluetoothDevice device, final int value) {
// empty default implementation
}
@Override
public void onError(final BluetoothDevice device, final String message, final int errorCode) {
DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode);
showToast(message + " (" + errorCode + ")");
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param message a message to be shown
*/
protected void showToast(final String message) {
runOnUiThread(() -> Toast.makeText(BleMulticonnectProfileServiceReadyActivity.this, message, Toast.LENGTH_LONG).show());
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param messageResId an resource id of the message to be shown
*/
protected void showToast(final int messageResId) {
runOnUiThread(() -> Toast.makeText(BleMulticonnectProfileServiceReadyActivity.this, messageResId, Toast.LENGTH_SHORT).show());
}
/**
* Returns the string resource id that will be shown in About box
*
* @return the about resource id
*/
protected abstract int getAboutTextId();
/**
* The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also:
* {@link #isChangingConfigurations()}.
*
* @return the required UUID or <code>null</code>
*/
protected abstract UUID getFilterUUID();
/**
* Returns unmodifiable list of managed devices. Managed device is a device the was selected on ScannerFragment until it's removed from the managed list.
* It does not have to be connected at that moment.
* @return unmodifiable list of managed devices
*/
protected List<BluetoothDevice> getManagedDevices() {
return Collections.unmodifiableList(mManagedDevices);
}
/**
* Returns <code>true</code> if the device is connected. Services may not have been discovered yet.
* @param device the device to check if it's connected
*/
protected boolean isDeviceConnected(final BluetoothDevice device) {
return mService != null && mService.isConnected(device);
}
/**
* Shows the scanner fragment.
*
* @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their
* services
* @see #getFilterUUID()
*/
private void showDeviceScanningDialog(final UUID filter) {
final ScannerFragment dialog = ScannerFragment.getInstance(filter);
dialog.show(getSupportFragmentManager(), "scan_fragment");
}
private void ensureBLESupported() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show();
finish();
}
}
protected boolean isBLEEnabled() {
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
final BluetoothAdapter adapter = bluetoothManager.getAdapter();
return adapter != null && adapter.isEnabled();
}
protected void showBLEDialog() {
final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2016, 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.profile.multiconnect;
import android.bluetooth.BluetoothDevice;
import androidx.annotation.StringRes;
public interface IDeviceLogger {
/**
* Logs the given message with given log level into the device's log session.
* @param device the target device
* @param level the log level
* @param message the message to be logged
*/
void log(final BluetoothDevice device, final int level, final String message);
/**
* Logs the given message with given log level into the device's log session.
* @param device the target device
* @param level the log level
* @param messageRes string resource id
* @param params additional (optional) parameters used to fill the message
*/
void log(final BluetoothDevice device, final int level, @StringRes final int messageRes, final Object... params);
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (c) 2016, 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.proximity;
import android.bluetooth.BluetoothDevice;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.List;
import no.nordicsemi.android.nrftoolbox.R;
public class DeviceAdapter extends RecyclerView.Adapter<DeviceAdapter.ViewHolder> {
private final ProximityService.ProximityBinder mService;
private final List<BluetoothDevice> mDevices;
DeviceAdapter(final ProximityService.ProximityBinder binder) {
mService = binder;
mDevices = mService.getManagedDevices();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_feature_proximity_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
holder.bind(mDevices.get(position));
}
@Override
public int getItemCount() {
return mDevices.size();
}
public void onDeviceAdded(final BluetoothDevice device) {
final int position = mDevices.indexOf(device);
if (position == -1) {
notifyItemInserted(mDevices.size() - 1);
} else {
// This may happen when Bluetooth adapter was switched off and on again
// while there were devices on the list.
notifyItemChanged(position);
}
}
public void onDeviceRemoved(final BluetoothDevice device) {
notifyDataSetChanged(); // we don't have position of the removed device here
}
public void onDeviceStateChanged(final BluetoothDevice device) {
final int position = mDevices.indexOf(device);
if (position >= 0)
notifyItemChanged(position);
}
public void onBatteryValueReceived(final BluetoothDevice device) {
final int position = mDevices.indexOf(device);
if (position >= 0)
notifyItemChanged(position);
}
class ViewHolder extends RecyclerView.ViewHolder {
private TextView nameView;
private TextView addressView;
private TextView batteryView;
private ImageButton actionButton;
private ProgressBar progress;
ViewHolder(final View itemView) {
super(itemView);
nameView = itemView.findViewById(R.id.name);
addressView = itemView.findViewById(R.id.address);
batteryView = itemView.findViewById(R.id.battery);
actionButton = itemView.findViewById(R.id.action_find_silent);
progress = itemView.findViewById(R.id.progress);
// Configure FIND / SILENT button
actionButton.setOnClickListener(v -> {
final int position = getAdapterPosition();
final BluetoothDevice device = mDevices.get(position);
mService.toggleImmediateAlert(device);
});
// Configure Disconnect button
itemView.findViewById(R.id.action_disconnect).setOnClickListener(v -> {
final int position = getAdapterPosition();
final BluetoothDevice device = mDevices.get(position);
mService.disconnect(device);
// The device might have not been connected, so there will be no callback
onDeviceRemoved(device);
});
}
private void bind(final BluetoothDevice device) {
final boolean ready = mService.isReady(device);
String name = device.getName();
if (TextUtils.isEmpty(name))
name = nameView.getResources().getString(R.string.proximity_default_device_name);
nameView.setText(name);
addressView.setText(device.getAddress());
final boolean on = mService.isImmediateAlertOn(device);
actionButton.setImageResource(on ? R.drawable.ic_stat_notify_proximity_silent : R.drawable.ic_stat_notify_proximity_find);
actionButton.setVisibility(ready ? View.VISIBLE : View.GONE);
progress.setVisibility(ready ? View.GONE : View.VISIBLE);
final Integer batteryValue = mService.getBatteryLevel(device);
if (batteryValue != null) {
batteryView.getCompoundDrawables()[0 /*left*/].setLevel(batteryValue);
batteryView.setVisibility(View.VISIBLE);
batteryView.setText(batteryView.getResources().getString(R.string.battery, batteryValue));
batteryView.setAlpha(ready ? 1.0f : 0.5f);
} else {
batteryView.setVisibility(View.GONE);
}
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.proximity;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import no.nordicsemi.android.nrftoolbox.R;
public class LinkLossFragment extends DialogFragment {
private static final String ARG_NAME = "name";
private String mName;
public static LinkLossFragment getInstance(String name) {
final LinkLossFragment fragment = new LinkLossFragment();
final Bundle args = new Bundle();
args.putString(ARG_NAME, name);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mName = getArguments().getString(ARG_NAME);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.app_name))
.setMessage(getString(R.string.proximity_notification_link_loss_alert, mName))
.setPositiveButton(R.string.ok, null)
.create();
}
}

View File

@@ -0,0 +1,191 @@
/*
* 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.proximity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.UUID;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileService;
import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileServiceReadyActivity;
import no.nordicsemi.android.nrftoolbox.widget.DividerItemDecoration;
public class ProximityActivity extends BleMulticonnectProfileServiceReadyActivity<ProximityService.ProximityBinder> {
private RecyclerView mDevicesView;
private DeviceAdapter mAdapter;
@Override
protected void onCreateView(final Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_proximity);
setGUI();
}
@Override
protected void onInitialize(final Bundle savedInstanceState) {
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, makeIntentFilter());
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
}
private void setGUI() {
final RecyclerView recyclerView = mDevicesView = findViewById(android.R.id.list);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
}
@Override
protected int getLoggerProfileTitle() {
return R.string.proximity_feature_title;
}
@Override
protected void onServiceBound(final ProximityService.ProximityBinder binder) {
mDevicesView.setAdapter(mAdapter = new DeviceAdapter(binder));
}
@Override
protected void onServiceUnbound() {
mDevicesView.setAdapter(mAdapter = null);
}
@Override
protected Class<? extends BleMulticonnectProfileService> getServiceClass() {
return ProximityService.class;
}
@Override
protected int getAboutTextId() {
return R.string.proximity_about_text;
}
@Override
protected UUID getFilterUUID() {
return ProximityManager.LINK_LOSS_SERVICE_UUID;
}
@Override
public void onDeviceConnecting(final BluetoothDevice device) {
if (mAdapter != null)
mAdapter.onDeviceAdded(device);
}
@Override
public void onDeviceConnected(final BluetoothDevice device) {
if (mAdapter != null)
mAdapter.onDeviceStateChanged(device);
}
@Override
public void onDeviceReady(final BluetoothDevice device) {
if (mAdapter != null)
mAdapter.onDeviceStateChanged(device);
}
@Override
public void onDeviceDisconnecting(final BluetoothDevice device) {
if (mAdapter != null)
mAdapter.onDeviceStateChanged(device);
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
if (mAdapter != null)
mAdapter.onDeviceRemoved(device);
}
@Override
public void onDeviceNotSupported(final BluetoothDevice device) {
super.onDeviceNotSupported(device);
if (mAdapter != null)
mAdapter.onDeviceRemoved(device);
}
@Override
public void onLinkLossOccurred(final BluetoothDevice device) {
if (mAdapter != null)
mAdapter.onDeviceStateChanged(device);
// The link loss may also be called when Bluetooth adapter was disabled
if (BluetoothAdapter.getDefaultAdapter().isEnabled())
showLinkLossDialog(device.getName());
}
@SuppressWarnings("unused")
private void onBatteryLevelChanged(final BluetoothDevice device, final int batteryLevel) {
if (mAdapter != null)
mAdapter.onBatteryValueReceived(device); // Value will be obtained from the service
}
@SuppressWarnings("unused")
private void onRemoteAlarmSwitched(final BluetoothDevice device, final boolean on) {
if (mAdapter != null)
mAdapter.onDeviceStateChanged(device); // Value will be obtained from the service
}
private void showLinkLossDialog(final String name) {
try {
final LinkLossFragment dialog = LinkLossFragment.getInstance(name);
dialog.show(getSupportFragmentManager(), "scan_fragment");
} catch (final Exception e) {
// the activity must have been destroyed
}
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
final BluetoothDevice device = intent.getParcelableExtra(ProximityService.EXTRA_DEVICE);
if (ProximityService.BROADCAST_BATTERY_LEVEL.equals(action)) {
final int batteryLevel = intent.getIntExtra(ProximityService.EXTRA_BATTERY_LEVEL, 0);
// Update GUI
onBatteryLevelChanged(device, batteryLevel);
} else if (ProximityService.BROADCAST_ALARM_SWITCHED.equals(action)) {
final boolean on = intent.getBooleanExtra(ProximityService.EXTRA_ALARM_STATE, false);
// Update GUI
onRemoteAlarmSwitched(device, on);
}
}
};
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ProximityService.BROADCAST_BATTERY_LEVEL);
intentFilter.addAction(ProximityService.BROADCAST_ALARM_SWITCHED);
return intentFilter;
}
}

View File

@@ -0,0 +1,149 @@
/*
* 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.proximity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import java.util.UUID;
import no.nordicsemi.android.ble.callback.FailCallback;
import no.nordicsemi.android.ble.common.data.alert.AlertLevelData;
import no.nordicsemi.android.ble.error.GattError;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.AlertLevelParser;
@SuppressWarnings("WeakerAccess")
class ProximityManager extends BatteryManager<ProximityManagerCallbacks> {
/** Link Loss service UUID. */
final static UUID LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb");
/** Immediate Alert service UUID. */
private final static UUID IMMEDIATE_ALERT_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb");
/** Alert Level characteristic UUID. */
private static final UUID ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mAlertLevelCharacteristic, mLinkLossCharacteristic;
private boolean mAlertOn;
ProximityManager(final Context context) {
super(context);
}
@Override
protected boolean shouldAutoConnect() {
return true;
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
super.initialize();
writeCharacteristic(mLinkLossCharacteristic, AlertLevelData.highAlert())
.done(device -> log(Log.INFO, "Link loss alert level set"))
.fail((device, status) -> log(Log.WARN, "Failed to set link loss level: " + status))
.enqueue();
}
@Override
protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService llService = gatt.getService(LINK_LOSS_SERVICE_UUID);
if (llService != null) {
mLinkLossCharacteristic = llService.getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID);
}
return mLinkLossCharacteristic != null;
}
@Override
protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
super.isOptionalServiceSupported(gatt);
final BluetoothGattService iaService = gatt.getService(IMMEDIATE_ALERT_SERVICE_UUID);
if (iaService != null) {
mAlertLevelCharacteristic = iaService.getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID);
}
return mAlertLevelCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
super.onDeviceDisconnected();
mAlertLevelCharacteristic = null;
mLinkLossCharacteristic = null;
// Reset the alert flag
mAlertOn = false;
}
};
/**
* Toggles the immediate alert on the target device.
*/
public void toggleImmediateAlert() {
writeImmediateAlert(!mAlertOn);
}
/**
* Writes the HIGH ALERT or NO ALERT command to the target device.
*
* @param on true to enable the alarm on proximity tag, false to disable it.
*/
public void writeImmediateAlert(final boolean on) {
if (!isConnected())
return;
writeCharacteristic(mAlertLevelCharacteristic, on ? AlertLevelData.highAlert() : AlertLevelData.noAlert())
.before(device -> log(Log.VERBOSE,
on ? "Setting alarm to HIGH..." : "Disabling alarm..."))
.with((device, data) -> log(LogContract.Log.Level.APPLICATION,
"\"" + AlertLevelParser.parse(data) + "\" sent"))
.done(device -> {
mAlertOn = on;
mCallbacks.onRemoteAlarmSwitched(device, on);
})
.fail((device, status) -> log(Log.WARN,
status == FailCallback.REASON_NULL_ATTRIBUTE ?
"Alert Level characteristic not found" :
GattError.parse(status)))
.enqueue();
}
/**
* Returns true if the alert has been enabled on the proximity tag, false otherwise.
*/
boolean isAlertEnabled() {
return mAlertOn;
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.proximity;
import android.bluetooth.BluetoothDevice;
import androidx.annotation.NonNull;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
interface ProximityManagerCallbacks extends BatteryManagerCallbacks {
// No additional methods
void onRemoteAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on);
}

View File

@@ -0,0 +1,386 @@
/*
* 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.proximity;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import java.util.UUID;
import no.nordicsemi.android.error.GattError;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.parser.AlertLevelParser;
import no.nordicsemi.android.nrftoolbox.profile.multiconnect.IDeviceLogger;
import no.nordicsemi.android.nrftoolbox.utility.ParserUtils;
class ProximityServerManager {
private final String TAG = "ProximityServerManager";
/** Immediate Alert service UUID */
final static UUID IMMEDIATE_ALERT_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb");
/** Linkloss service UUID */
final static UUID LINKLOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb");
/** Alert Level characteristic UUID */
private static final UUID ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb");
private final static byte[] HIGH_ALERT = { 0x02 };
private final static byte[] MILD_ALERT = { 0x01 };
private final static byte[] NO_ALERT = { 0x00 };
private BluetoothGattServer mBluetoothGattServer;
private ProximityServerManagerCallbacks mCallbacks;
private IDeviceLogger mLogger;
private Handler mHandler;
private OnServerOpenCallback mOnServerOpenCallback;
private boolean mServerReady;
public interface OnServerOpenCallback {
/**
* Method called when the GATT server was created and all services were added successfully.
*/
void onGattServerOpen();
/**
* Method called when the GATT server failed to open and initialize services.
* -1 is returned when the server failed to start.
*/
void onGattServerFailed(final int error);
}
ProximityServerManager(final ProximityServerManagerCallbacks callbacks) {
mHandler = new Handler();
mCallbacks = callbacks;
}
/**
* Sets the logger object. Logger is used to create logs in nRF Logger application.
*
* @param logger the logger object
*/
public void setLogger(final IDeviceLogger logger) {
mLogger = logger;
}
/**
* Opens GATT server and creates 2 services: Link Loss Service and Immediate Alert Service.
* The callback is called when initialization is complete.
*
* @param context the context.
* @param callback optional callback notifying when all services has been added.
*/
public void openGattServer(final Context context, final OnServerOpenCallback callback) {
// Is the server already open?
if (mBluetoothGattServer != null) {
if (callback != null)
callback.onGattServerOpen();
return;
}
mOnServerOpenCallback = callback;
final BluetoothManager manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothGattServer = manager.openGattServer(context, mGattServerCallbacks);
if (mBluetoothGattServer != null) {
// Start adding services one by one. The onServiceAdded method will be called when it completes.
addImmediateAlertService();
} else {
if (callback != null)
callback.onGattServerFailed(-1);
mOnServerOpenCallback = null;
}
}
/**
* Returns true if GATT server was opened and configured correctly.
* False if hasn't been opened, was closed, of failed to start.
*/
public boolean isServerReady() {
return mServerReady;
}
/**
* Closes the GATT server. It will also disconnect all existing connections.
* If the service has already been closed, or hasn't been open, this method does nothing.
*/
public void closeGattServer() {
if (mBluetoothGattServer != null) {
mBluetoothGattServer.close();
mBluetoothGattServer = null;
mOnServerOpenCallback = null;
mServerReady = false;
}
}
/**
* This method notifies the Android that the Proximity profile will use the server connection
* to given device. If the server hasn't been open this method does nothing.
* The {@link #cancelConnection(BluetoothDevice)} method should be called when the connection
* is no longer used.
*
* @param device the target device.
*/
public void openConnection(final BluetoothDevice device) {
if (mBluetoothGattServer != null) {
mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Creating server connection...");
mLogger.log(device, LogContract.Log.Level.DEBUG, "server.connect(device, autoConnect = true)");
mBluetoothGattServer.connect(device, true); // In proximity the autoConnect is true
}
}
/**
* Cancels the connection to the given device. This notifies Android that this profile will
* no longer use this connection and it can be disconnected. In practice, this method does
* not disconnect, so if the remote device decides still to use the phone's GATT server it
* will be able to do so.
* <p>
* This bug/feature can be tested using a proximity tag that does not release its connection
* when it got disconnected:
* <ol>
* <li>Connect to your Proximity Tag.</li>
* <li>Verify that the bidirectional connection works - test the FIND ME button in
* nRF Toolbox and the FIND PHONE button on the tag.</li>
* <li>Disconnect from the tag</li>
* <li>When the device disappear from the list of devices click the FIND PHONE button on
* the tag. Your phone should still trigger an alarm, as the connection tag-&gt;phone
* is still active.</li>
* </ol>
* In order to avoid this issue make sure that your tag disconnects gently from phone when it
* got disconnected itself.
*
* @param device the device that will no longer be used.
*/
public void cancelConnection(final BluetoothDevice device) {
if (mBluetoothGattServer != null) {
mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Cancelling server connection...");
mLogger.log(device, LogContract.Log.Level.DEBUG, "server.cancelConnection(device)");
mBluetoothGattServer.cancelConnection(device);
}
}
private void addImmediateAlertService() {
/*
* This method must be called in UI thread. It works fine on Nexus devices but if called
* from other thread (e.g. from onServiceAdded in gatt server callback) it hangs the app.
*/
final BluetoothGattCharacteristic alertLevel =
new BluetoothGattCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
alertLevel.setValue(NO_ALERT);
final BluetoothGattService immediateAlertService =
new BluetoothGattService(IMMEDIATE_ALERT_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
immediateAlertService.addCharacteristic(alertLevel);
mBluetoothGattServer.addService(immediateAlertService);
}
private void addLinkLossService() {
/*
* This method must be called in UI thread. It works fine on Nexus devices but if called
* from other thread (e.g. from onServiceAdded in gatt server callback) it hangs the app.
*/
final BluetoothGattCharacteristic linkLossAlertLevel =
new BluetoothGattCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ);
linkLossAlertLevel.setValue(HIGH_ALERT);
final BluetoothGattService linkLossService =
new BluetoothGattService(LINKLOSS_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
linkLossService.addCharacteristic(linkLossAlertLevel);
mBluetoothGattServer.addService(linkLossService);
}
private final BluetoothGattServerCallback mGattServerCallbacks = new BluetoothGattServerCallback() {
@Override
public void onServiceAdded(final int status, final BluetoothGattService service) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// Adding another service from callback thread fails on Samsung S4 with Android 4.3
mHandler.post(() -> {
if (IMMEDIATE_ALERT_SERVICE_UUID.equals(service.getUuid())) {
addLinkLossService();
} else {
mServerReady = true;
// Both services has been added
if (mOnServerOpenCallback != null)
mOnServerOpenCallback.onGattServerOpen();
mOnServerOpenCallback = null;
}
});
} else {
Log.e(TAG, "GATT Server failed to add service, status: " + status);
if (mOnServerOpenCallback != null)
mOnServerOpenCallback.onGattServerFailed(status);
mOnServerOpenCallback = null;
}
}
@Override
public void onConnectionStateChange(final BluetoothDevice device, final int status, final int newState) {
mLogger.log(device, LogContract.Log.Level.DEBUG,
"[Server callback] Connection state changed with status: " + status
+ " and new state: " + newState + " (" + stateToString(newState) + ")");
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
mLogger.log(device, LogContract.Log.Level.INFO,
"[Server] Device with address " + device.getAddress() + " connected");
} else {
mLogger.log(device, LogContract.Log.Level.INFO, "[Server] Device disconnected");
mCallbacks.onAlarmStopped(device);
}
} else {
mLogger.log(device, LogContract.Log.Level.ERROR, "[Server] Error " + status +
" (0x" + Integer.toHexString(status) + "): " + GattError.parseConnectionError(status));
}
}
@Override
public void onCharacteristicReadRequest(final BluetoothDevice device, final int requestId,
final int offset, final BluetoothGattCharacteristic characteristic) {
mLogger.log(device, LogContract.Log.Level.DEBUG,
"[Server callback] Read request for characteristic " + characteristic.getUuid()
+ " (requestId=" + requestId + ", offset=" + offset + ")");
mLogger.log(device, LogContract.Log.Level.INFO,
"[Server] READ request for characteristic " + characteristic.getUuid() + " received");
byte[] value = characteristic.getValue();
if (value != null && offset > 0) {
byte[] offsetValue = new byte[value.length - offset];
System.arraycopy(value, offset, offsetValue, 0, offsetValue.length);
value = offsetValue;
}
if (value != null) {
mLogger.log(device, LogContract.Log.Level.DEBUG,
"server.sendResponse(GATT_SUCCESS, value=" + ParserUtils.parseDebug(value) + ")");
} else {
mLogger.log(device, LogContract.Log.Level.DEBUG, "server.sendResponse(GATT_SUCCESS, value=null)");
}
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent");
}
@Override
public void onCharacteristicWriteRequest(final BluetoothDevice device, final int requestId,
final BluetoothGattCharacteristic characteristic, final boolean preparedWrite,
final boolean responseNeeded, final int offset, final byte[] value) {
mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Write request to characteristic " + characteristic.getUuid()
+ " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded=" + responseNeeded
+ ", offset=" + offset + ", value=" + ParserUtils.parseDebug(value) + ")");
final String writeType = !responseNeeded ? "WRITE NO RESPONSE" : "WRITE COMMAND";
mLogger.log(device, LogContract.Log.Level.INFO, "[Server] " + writeType
+ " request for characteristic " + characteristic.getUuid() + " received, value: " + ParserUtils.parse(value));
if (offset == 0) {
characteristic.setValue(value);
} else {
final byte[] currentValue = characteristic.getValue();
final byte[] newValue = new byte[currentValue.length + value.length];
System.arraycopy(currentValue, 0, newValue, 0, currentValue.length);
System.arraycopy(value, 0, newValue, offset, value.length);
characteristic.setValue(newValue);
}
if (!preparedWrite && value != null && value.length == 1) { // small validation
if (value[0] != NO_ALERT[0]) {
mLogger.log(device, LogContract.Log.Level.APPLICATION,
"[Server] Immediate alarm request received: " + AlertLevelParser.parse(characteristic));
mCallbacks.onAlarmTriggered(device);
} else {
mLogger.log(device, LogContract.Log.Level.APPLICATION,
"[Server] Immediate alarm request received: OFF");
mCallbacks.onAlarmStopped(device);
}
}
mLogger.log(device, LogContract.Log.Level.DEBUG, "server.sendResponse(GATT_SUCCESS, offset="
+ offset + ", value=" + ParserUtils.parseDebug(value) + ")");
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent");
}
@Override
public void onDescriptorReadRequest(final BluetoothDevice device, final int requestId,
final int offset, final BluetoothGattDescriptor descriptor) {
mLogger.log(device, LogContract.Log.Level.DEBUG,
"[Server callback] Write request to descriptor " + descriptor.getUuid() + " (requestId=" + requestId + ", offset=" + offset + ")");
mLogger.log(device, LogContract.Log.Level.INFO,
"[Server] READ request for descriptor " + descriptor.getUuid() + " received");
// This method is not supported
mLogger.log(device, LogContract.Log.Level.WARNING, "[Server] Operation not supported");
mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)");
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null);
mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent");
}
@Override
public void onDescriptorWriteRequest(final BluetoothDevice device, final int requestId,
final BluetoothGattDescriptor descriptor, final boolean preparedWrite,
final boolean responseNeeded, final int offset, final byte[] value) {
mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Write request to descriptor " + descriptor.getUuid()
+ " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded=" + responseNeeded
+ ", offset=" + offset + ", value=" + ParserUtils.parse(value) + ")");
mLogger.log(device, LogContract.Log.Level.INFO, "[Server] READ request for descriptor " + descriptor.getUuid() + " received");
// This method is not supported
mLogger.log(device, LogContract.Log.Level.WARNING, "[Server] Operation not supported");
mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)");
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null);
mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent");
}
@Override
public void onExecuteWrite(final BluetoothDevice device, final int requestId, final boolean execute) {
mLogger.log(device, LogContract.Log.Level.DEBUG,
"[Server callback] Execute write request (requestId=" + requestId + ", execute=" + execute + ")");
// This method is not supported
mLogger.log(device, LogContract.Log.Level.WARNING, "[Server] Operation not supported");
mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)");
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, 0, null);
mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent");
}
};
/**
* Converts the connection state to String value.
*
* @param state the connection state.
* @return The state as String.
*/
private String stateToString(final int state) {
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
return "CONNECTED";
case BluetoothProfile.STATE_CONNECTING:
return "CONNECTING";
case BluetoothProfile.STATE_DISCONNECTING:
return "DISCONNECTING";
default:
return "DISCONNECTED";
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.proximity;
import android.bluetooth.BluetoothDevice;
import androidx.annotation.NonNull;
public interface ProximityServerManagerCallbacks {
void onAlarmTriggered(@NonNull final BluetoothDevice device);
void onAlarmStopped(@NonNull final BluetoothDevice device);
}

View File

@@ -0,0 +1,570 @@
/*
* 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.proximity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.FeaturesActivity;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.ToolboxApplication;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileService;
public class ProximityService extends BleMulticonnectProfileService implements ProximityManagerCallbacks, ProximityServerManagerCallbacks {
@SuppressWarnings("unused")
private static final String TAG = "ProximityService";
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
public static final String BROADCAST_ALARM_SWITCHED = "no.nordicsemi.android.nrftoolbox.BROADCAST_ALARM_SWITCHED";
public static final String EXTRA_ALARM_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_ALARM_STATE";
private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_DISCONNECT";
private final static String ACTION_FIND = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_FIND";
private final static String ACTION_SILENT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_SILENT";
private final static String PROXIMITY_GROUP_ID = "proximity_connected_tags";
private final static int NOTIFICATION_ID = 1000;
private final static int OPEN_ACTIVITY_REQ = 0;
private final static int DISCONNECT_REQ = 1;
private final static int FIND_REQ = 2;
private final static int SILENT_REQ = 3;
private final ProximityBinder mBinder = new ProximityBinder();
private ProximityServerManager mServerManager;
private MediaPlayer mMediaPlayer;
private int mOriginalVolume;
/**
* When a device starts an alarm on the phone it is added to this list.
* Alarm is disabled when this list is empty.
*/
private List<BluetoothDevice> mDevicesWithAlarm;
private int mAttempt;
private final static int MAX_ATTEMPTS = 1;
/**
* This local binder is an interface for the bonded activity to operate with the proximity
* sensor.
*/
public class ProximityBinder extends LocalBinder {
/**
* Toggles the Immediate Alert on given remote device.
*
* @param device the connected device.
*/
public void toggleImmediateAlert(final BluetoothDevice device) {
final ProximityManager manager = (ProximityManager) getBleManager(device);
manager.toggleImmediateAlert();
}
/**
* Returns the current alarm state on given device. This value is not read from the device,
* it's just the last value written to it (initially false).
*
* @param device the connected device.
* @return True if alarm has been enabled, false if disabled.
*/
public boolean isImmediateAlertOn(final BluetoothDevice device) {
final ProximityManager manager = (ProximityManager) getBleManager(device);
return manager.isAlertEnabled();
}
/**
* Returns the last received battery level value.
*
* @param device the device of which battery level should be returned.
* @return Battery value or null if no value was received or Battery Level characteristic
* was not found, or the device is disconnected.
*/
public Integer getBatteryLevel(final BluetoothDevice device) {
final ProximityManager manager = (ProximityManager) getBleManager(device);
return manager.getBatteryLevel();
}
}
@Override
protected LocalBinder getBinder() {
return mBinder;
}
@Override
protected LoggableBleManager<ProximityManagerCallbacks> initializeManager() {
return new ProximityManager(this);
}
/**
* 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) {
final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE);
mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] DISCONNECT action pressed");
mBinder.disconnect(device);
}
};
/**
* This broadcast receiver listens for {@link #ACTION_FIND} or {@link #ACTION_SILENT} that may
* be fired by pressing Find me action button on the notification.
*/
private final BroadcastReceiver mToggleAlarmActionBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE);
switch (intent.getAction()) {
case ACTION_FIND:
mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] FIND action pressed");
break;
case ACTION_SILENT:
mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] SILENT action pressed");
break;
}
mBinder.toggleImmediateAlert(device);
}
};
@Override
protected void onServiceCreated() {
mServerManager = new ProximityServerManager(this);
mServerManager.setLogger(mBinder);
initializeAlarm();
registerReceiver(mDisconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT));
final IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_FIND);
filter.addAction(ACTION_SILENT);
registerReceiver(mToggleAlarmActionBroadcastReceiver, filter);
}
@Override
public void onServiceStopped() {
cancelNotifications();
// Close the GATT server. If it hasn't been opened this method does nothing
mServerManager.closeGattServer();
releaseAlarm();
unregisterReceiver(mDisconnectActionBroadcastReceiver);
unregisterReceiver(mToggleAlarmActionBroadcastReceiver);
super.onServiceStopped();
}
@Override
protected void onBluetoothEnabled() {
mAttempt = 0;
getHandler().post(new Runnable() {
@Override
public void run() {
final Runnable that = this;
// Start the GATT Server only if Bluetooth is enabled
mServerManager.openGattServer(ProximityService.this,
new ProximityServerManager.OnServerOpenCallback() {
@Override
public void onGattServerOpen() {
// We are now ready to reconnect devices
ProximityService.super.onBluetoothEnabled();
}
@Override
public void onGattServerFailed(final int error) {
mServerManager.closeGattServer();
if (mAttempt < MAX_ATTEMPTS) {
mAttempt++;
getHandler().postDelayed(that, 2000);
} else {
showToast(getString(R.string.proximity_server_error, error));
// GATT server failed to start, but we may connect as a client
ProximityService.super.onBluetoothEnabled();
}
}
});
}
});
}
@Override
protected void onBluetoothDisabled() {
super.onBluetoothDisabled();
// Close the GATT server
mServerManager.closeGattServer();
}
@Override
protected void onRebind() {
// When the activity rebinds to the service, remove the notification
cancelNotifications();
// This method will read the Battery Level value from each connected device, if possible
// and then try to enable battery notifications (if it has NOTIFY property).
// If the Battery Level characteristic has only the NOTIFY property, it will only try to
// enable notifications.
for (final BluetoothDevice device : getManagedDevices()) {
final ProximityManager manager = (ProximityManager) getBleManager(device);
manager.readBatteryLevelCharacteristic();
manager.enableBatteryLevelCharacteristicNotifications();
}
}
@Override
public void onUnbind() {
// When we are connected, but the application is not open, we are not really interested
// in battery level notifications. But we will still be receiving other values, if enabled.
for (final BluetoothDevice device : getManagedDevices()) {
final ProximityManager manager = (ProximityManager) getBleManager(device);
manager.disableBatteryLevelCharacteristicNotifications();
}
createBackgroundNotification();
}
@Override
public void onDeviceConnected(final BluetoothDevice device) {
super.onDeviceConnected(device);
if (!mBound) {
createBackgroundNotification();
}
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
super.onServicesDiscovered(device, optionalServicesFound);
mServerManager.openConnection(device);
}
@Override
public void onLinkLossOccurred(final BluetoothDevice device) {
mServerManager.cancelConnection(device);
stopAlarm(device);
super.onLinkLossOccurred(device);
if (!mBound) {
createBackgroundNotification();
if (BluetoothAdapter.getDefaultAdapter().isEnabled())
createLinkLossNotification(device);
else
cancelNotification(device);
}
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
mServerManager.cancelConnection(device);
stopAlarm(device);
super.onDeviceDisconnected(device);
if (!mBound) {
cancelNotification(device);
createBackgroundNotification();
}
}
@Override
public void onAlarmTriggered(@NonNull final BluetoothDevice device) {
playAlarm(device);
}
@Override
public void onAlarmStopped(@NonNull final BluetoothDevice device) {
stopAlarm(device);
}
@Override
public void onRemoteAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on) {
final Intent broadcast = new Intent(BROADCAST_ALARM_SWITCHED);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_ALARM_STATE, on);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
if (!mBound) {
createBackgroundNotification();
}
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, device);
broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
private void createBackgroundNotification() {
final List<BluetoothDevice> connectedDevices = getConnectedDevices();
for (final BluetoothDevice device : connectedDevices) {
createNotificationForConnectedDevice(device);
}
createSummaryNotification();
}
private void createSummaryNotification() {
final NotificationCompat.Builder builder = getNotificationBuilder();
builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark));
builder.setShowWhen(false).setDefaults(0);
// An ongoing notification will not be shown on Android Wear.
builder.setOngoing(true);
builder.setGroup(PROXIMITY_GROUP_ID).setGroupSummary(true);
builder.setContentTitle(getString(R.string.app_name));
final List<BluetoothDevice> managedDevices = getManagedDevices();
final List<BluetoothDevice> connectedDevices = getConnectedDevices();
if (connectedDevices.isEmpty()) {
// No connected devices
final int numberOfManagedDevices = managedDevices.size();
if (numberOfManagedDevices == 1) {
final String name = getDeviceName(managedDevices.get(0));
// We don't use plurals here, as we only have the default language and 'one' is not
// in every language (versions differ in %d or %s) and throw an exception in e.g. in Chinese.
builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name));
} else {
builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfManagedDevices));
}
} else {
// There are some proximity tags connected
final StringBuilder text = new StringBuilder();
final int numberOfConnectedDevices = connectedDevices.size();
if (numberOfConnectedDevices == 1) {
final String name = getDeviceName(connectedDevices.get(0));
text.append(getString(R.string.proximity_notification_summary_text_name, name));
} else {
text.append(getString(R.string.proximity_notification_summary_text_number, numberOfConnectedDevices));
}
// If there are some disconnected devices, also print them
final int numberOfDisconnectedDevices = managedDevices.size() - numberOfConnectedDevices;
if (numberOfDisconnectedDevices == 1) {
text.append(", ");
// Find the single disconnected device to get its name
for (final BluetoothDevice device : managedDevices) {
if (!isConnected(device)) {
final String name = getDeviceName(device);
text.append(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name));
break;
}
}
} else if (numberOfDisconnectedDevices > 1) {
text.append(", ");
// If there are more, just write number of them
text.append(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfDisconnectedDevices));
}
text.append(".");
builder.setContentText(text);
}
final Notification notification = builder.build();
final NotificationManagerCompat nm = NotificationManagerCompat.from(this);
nm.notify(NOTIFICATION_ID, notification);
}
/**
* Creates the notification for given connected device.
* Adds 3 action buttons: DISCONNECT, FIND and SILENT which perform given action on the device.
*/
private void createNotificationForConnectedDevice(final BluetoothDevice device) {
final NotificationCompat.Builder builder = getNotificationBuilder();
builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark));
builder.setGroup(PROXIMITY_GROUP_ID).setDefaults(0);
// An ongoing notification will not be shown on Android Wear.
builder.setOngoing(true);
builder.setContentTitle(getString(R.string.proximity_notification_text, getDeviceName(device)));
// Add DISCONNECT action
final Intent disconnect = new Intent(ACTION_DISCONNECT);
disconnect.putExtra(EXTRA_DEVICE, device);
final PendingIntent disconnectAction =
PendingIntent.getBroadcast(this, DISCONNECT_REQ + device.hashCode(),
disconnect, PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.proximity_action_disconnect), disconnectAction));
// This will keep the same order of notification even after an action was clicked on one of them.
builder.setSortKey(getDeviceName(device) + device.getAddress());
// Add FIND or SILENT action
final ProximityManager manager = (ProximityManager) getBleManager(device);
if (manager.isAlertEnabled()) {
final Intent silentAllIntent = new Intent(ACTION_SILENT);
silentAllIntent.putExtra(EXTRA_DEVICE, device);
final PendingIntent silentAction =
PendingIntent.getBroadcast(this, SILENT_REQ + device.hashCode(),
silentAllIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_silent, getString(R.string.proximity_action_silent), silentAction));
} else {
final Intent findAllIntent = new Intent(ACTION_FIND);
findAllIntent.putExtra(EXTRA_DEVICE, device);
final PendingIntent findAction =
PendingIntent.getBroadcast(this, FIND_REQ + device.hashCode(),
findAllIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_find, getString(R.string.proximity_action_find), findAction));
}
final Notification notification = builder.build();
final NotificationManagerCompat nm = NotificationManagerCompat.from(this);
nm.notify(device.getAddress(), NOTIFICATION_ID, notification);
}
/**
* Creates a notification showing information about a device that got disconnected.
*/
private void createLinkLossNotification(final BluetoothDevice device) {
final NotificationCompat.Builder builder = getNotificationBuilder();
builder.setColor(ContextCompat.getColor(this, R.color.orange));
final Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
// Make sure the sound is played even in DND mode
builder.setSound(notificationUri, AudioManager.STREAM_ALARM);
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
builder.setCategory(NotificationCompat.CATEGORY_ALARM);
builder.setShowWhen(true);
// An ongoing notification would not be shown on Android Wear.
builder.setOngoing(false);
// This notification is to be shown not in a group
final String name = getDeviceName(device);
builder.setContentTitle(getString(R.string.proximity_notification_link_loss_alert, name));
builder.setTicker(getString(R.string.proximity_notification_link_loss_alert, name));
final Notification notification = builder.build();
final NotificationManagerCompat nm = NotificationManagerCompat.from(this);
nm.notify(device.getAddress(), NOTIFICATION_ID, notification);
}
private NotificationCompat.Builder getNotificationBuilder() {
final Intent parentIntent = new Intent(this, FeaturesActivity.class);
parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final Intent targetIntent = new Intent(this, ProximityActivity.class);
// 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 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.PROXIMITY_WARNINGS_CHANNEL);
builder.setContentIntent(pendingIntent).setAutoCancel(false);
builder.setSmallIcon(R.drawable.ic_stat_notify_proximity);
return builder;
}
/**
* Cancels the existing notification. If there is no active notification this method does nothing
*/
private void cancelNotifications() {
final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(NOTIFICATION_ID);
final List<BluetoothDevice> managedDevices = getManagedDevices();
for (final BluetoothDevice device : managedDevices) {
nm.cancel(device.getAddress(), NOTIFICATION_ID);
}
}
/**
* Cancels the existing notification for given device. If there is no active notification this method does nothing
*/
private void cancelNotification(final BluetoothDevice device) {
final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(device.getAddress(), NOTIFICATION_ID);
}
private void initializeAlarm() {
mDevicesWithAlarm = new LinkedList<>();
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
mMediaPlayer.setLooping(true);
mMediaPlayer.setVolume(1.0f, 1.0f);
try {
mMediaPlayer.setDataSource(this, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM));
} catch (final IOException e) {
Log.e(TAG, "Initialize Alarm failed: ", e);
}
}
private void releaseAlarm() {
mMediaPlayer.release();
mMediaPlayer = null;
}
private void playAlarm(final BluetoothDevice device) {
final boolean alarmPlaying = !mDevicesWithAlarm.isEmpty();
if (!mDevicesWithAlarm.contains(device))
mDevicesWithAlarm.add(device);
if (!alarmPlaying) {
// Save the current alarm volume and set it to max
final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mOriginalVolume = am.getStreamVolume(AudioManager.STREAM_ALARM);
am.setStreamVolume(AudioManager.STREAM_ALARM, am.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
try {
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (final IOException e) {
Log.e(TAG, "Prepare Alarm failed: ", e);
}
}
}
private void stopAlarm(final BluetoothDevice device) {
mDevicesWithAlarm.remove(device);
if (mDevicesWithAlarm.isEmpty() && mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
// Restore original volume
final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.setStreamVolume(AudioManager.STREAM_ALARM, mOriginalVolume, 0);
}
}
private String getDeviceName(final BluetoothDevice device) {
String name = device.getName();
if (TextUtils.isEmpty(name))
name = getString(R.string.proximity_default_device_name);
return name;
}
}

View File

@@ -0,0 +1,288 @@
/*
* 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.rsc;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.view.Menu;
import android.widget.TextView;
import java.util.Locale;
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.rsc.settings.SettingsActivity;
import no.nordicsemi.android.nrftoolbox.rsc.settings.SettingsFragment;
public class RSCActivity extends BleProfileServiceReadyActivity<RSCService.RSCBinder> {
private TextView mSpeedView;
private TextView mSpeedUnitView;
private TextView mCadenceView;
private TextView mDistanceView;
private TextView mDistanceUnitView;
private TextView mTotalDistanceView;
private TextView mTotalDistanceUnitView;
private TextView mStridesCountView;
private TextView mActivityView;
private TextView mBatteryLevelView;
@Override
protected void onCreateView(final Bundle savedInstanceState) {
setContentView(R.layout.activity_feature_rsc);
setGui();
}
@Override
protected void onInitialize(final Bundle savedInstanceState) {
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, makeIntentFilter());
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
}
private void setGui() {
mSpeedView = findViewById(R.id.speed);
mSpeedUnitView = findViewById(R.id.speed_unit);
mCadenceView = findViewById(R.id.cadence);
mDistanceView = findViewById(R.id.distance);
mDistanceUnitView = findViewById(R.id.distance_unit);
mTotalDistanceView = findViewById(R.id.total_distance);
mTotalDistanceUnitView = findViewById(R.id.total_distance_unit);
mStridesCountView = findViewById(R.id.strides);
mActivityView = findViewById(R.id.activity);
mBatteryLevelView = findViewById(R.id.battery);
}
@Override
protected void onResume() {
super.onResume();
setDefaultUI();
}
@Override
protected void setDefaultUI() {
mSpeedView.setText(R.string.not_available_value);
mCadenceView.setText(R.string.not_available_value);
mDistanceView.setText(R.string.not_available_value);
mTotalDistanceView.setText(R.string.not_available_value);
mStridesCountView.setText(R.string.not_available_value);
mActivityView.setText(R.string.not_available);
mBatteryLevelView.setText(R.string.not_available);
setUnits();
}
private void setUnits() {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT)));
switch (unit) {
case SettingsFragment.SETTINGS_UNIT_M_S: // [m/s]
mSpeedUnitView.setText(R.string.rsc_speed_unit_m_s);
mDistanceUnitView.setText(R.string.rsc_distance_unit_m);
mTotalDistanceUnitView.setText(R.string.rsc_total_distance_unit_km);
break;
case SettingsFragment.SETTINGS_UNIT_KM_H: // [km/h]
mSpeedUnitView.setText(R.string.rsc_speed_unit_km_h);
mDistanceUnitView.setText(R.string.rsc_distance_unit_m);
mTotalDistanceUnitView.setText(R.string.rsc_total_distance_unit_km);
break;
case SettingsFragment.SETTINGS_UNIT_MPH: // [mph]
mSpeedUnitView.setText(R.string.rsc_speed_unit_mph);
mDistanceUnitView.setText(R.string.rsc_distance_unit_yd);
mTotalDistanceUnitView.setText(R.string.rsc_total_distance_unit_mile);
break;
}
}
@Override
protected int getLoggerProfileTitle() {
return R.string.rsc_feature_title;
}
@Override
protected int getDefaultDeviceName() {
return R.string.rsc_default_name;
}
@Override
protected int getAboutTextId() {
return R.string.rsc_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 Class<? extends BleProfileService> getServiceClass() {
return RSCService.class;
}
@Override
protected UUID getFilterUUID() {
return RSCManager.RUNNING_SPEED_AND_CADENCE_SERVICE_UUID;
}
@Override
protected void onServiceBound(final RSCService.RSCBinder binder) {
// not used
}
@Override
protected void onServiceUnbound() {
// not used
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
// not used
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
super.onDeviceDisconnected(device);
mBatteryLevelView.setText(R.string.not_available);
}
private void onMeasurementReceived(float speed, int cadence, long totalDistance, final boolean running) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT)));
switch (unit) {
case SettingsFragment.SETTINGS_UNIT_KM_H:
speed = speed * 3.6f;
// pass through intended
case SettingsFragment.SETTINGS_UNIT_M_S:
if (totalDistance == -1) {
mTotalDistanceView.setText(R.string.not_available);
mTotalDistanceUnitView.setText(null);
} else {
mTotalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1000.0f)); // 1 km in m
mTotalDistanceUnitView.setText(R.string.rsc_total_distance_unit_km);
}
break;
case SettingsFragment.SETTINGS_UNIT_MPH:
speed = speed * 2.2369f;
if (totalDistance == -1) {
mTotalDistanceView.setText(R.string.not_available);
mTotalDistanceUnitView.setText(null);
} else {
mTotalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1609.31f)); // 1 mile in m
mTotalDistanceUnitView.setText(R.string.rsc_total_distance_unit_mile);
}
break;
}
mSpeedView.setText(String.format(Locale.US, "%.1f", speed));
mCadenceView.setText(String.format(Locale.US, "%d", cadence));
mActivityView.setText(running ? R.string.rsc_running : R.string.rsc_walking);
}
private void onStripesUpdate(final long distance, final int strides) {
if (distance == -1) {
mDistanceView.setText(R.string.not_available);
mDistanceUnitView.setText(R.string.rsc_distance_unit_m);
} else {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT)));
switch (unit) {
case SettingsFragment.SETTINGS_UNIT_KM_H:
case SettingsFragment.SETTINGS_UNIT_M_S:
if (distance < 100000L) { // 1 km in cm
mDistanceView.setText(String.format(Locale.US, "%.1f", distance / 100.0f));
mDistanceUnitView.setText(R.string.rsc_distance_unit_m);
} else {
mDistanceView.setText(String.format(Locale.US, "%.2f", distance / 100000.0f));
mDistanceUnitView.setText(R.string.rsc_distance_unit_km);
}
break;
case SettingsFragment.SETTINGS_UNIT_MPH:
if (distance < 160931L) { // 1 mile in cm
mDistanceView.setText(String.format(Locale.US, "%.1f", distance / 91.4392f));
mDistanceUnitView.setText(R.string.rsc_distance_unit_yd);
} else {
mDistanceView.setText(String.format(Locale.US, "%.2f", distance / 160931.23f));
mDistanceUnitView.setText(R.string.rsc_distance_unit_mile);
}
break;
}
}
mStridesCountView.setText(String.valueOf(strides));
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
if (RSCService.BROADCAST_RSC_MEASUREMENT.equals(action)) {
final float speed = intent.getFloatExtra(RSCService.EXTRA_SPEED, 0.0f);
final int cadence = intent.getIntExtra(RSCService.EXTRA_CADENCE, 0);
final long totalDistance = intent.getLongExtra(RSCService.EXTRA_TOTAL_DISTANCE, -1);
final boolean running = intent.getBooleanExtra(RSCService.EXTRA_ACTIVITY, false);
// Update GUI
onMeasurementReceived(speed, cadence, totalDistance, running);
} else if (RSCService.BROADCAST_STRIDES_UPDATE.equals(action)) {
final int strides = intent.getIntExtra(RSCService.EXTRA_STRIDES, 0);
final long distance = intent.getLongExtra(RSCService.EXTRA_DISTANCE, -1);
// Update GUI
onStripesUpdate(distance, strides);
}
}
};
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(RSCService.BROADCAST_RSC_MEASUREMENT);
intentFilter.addAction(RSCService.BROADCAST_STRIDES_UPDATE);
return intentFilter;
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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.rsc;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.UUID;
import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.RSCMeasurementParser;
public class RSCManager extends BatteryManager<RSCManagerCallbacks> {
/** Running Speed and Cadence Measurement service UUID */
public static final UUID RUNNING_SPEED_AND_CADENCE_SERVICE_UUID = UUID.fromString("00001814-0000-1000-8000-00805f9b34fb");
/** Running Speed and Cadence Measurement characteristic UUID */
private static final UUID RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mRSCMeasurementCharacteristic;
RSCManager(final Context context) {
super(context);
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
super.initialize();
setNotificationCallback(mRSCMeasurementCharacteristic)
.with(new RunningSpeedAndCadenceMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + RSCMeasurementParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onRSCMeasurementReceived(@NonNull final BluetoothDevice device, final boolean running,
final float instantaneousSpeed, final int instantaneousCadence,
@Nullable final Integer strideLength,
@Nullable final Long totalDistance) {
mCallbacks.onRSCMeasurementReceived(device, running, instantaneousSpeed,
instantaneousCadence, strideLength, totalDistance);
}
});
enableNotifications(mRSCMeasurementCharacteristic).enqueue();
}
@Override
public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(RUNNING_SPEED_AND_CADENCE_SERVICE_UUID);
if (service != null) {
mRSCMeasurementCharacteristic = service.getCharacteristic(RSC_MEASUREMENT_CHARACTERISTIC_UUID);
}
return mRSCMeasurementCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
super.onDeviceDisconnected();
mRSCMeasurementCharacteristic = null;
}
};
}

View File

@@ -0,0 +1,29 @@
/*
* 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.rsc;
import no.nordicsemi.android.ble.common.profile.rsc.RunningSpeedAndCadenceMeasurementCallback;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
interface RSCManagerCallbacks extends BatteryManagerCallbacks, RunningSpeedAndCadenceMeasurementCallback {
}

View File

@@ -0,0 +1,252 @@
/*
* 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.rsc;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.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.ToolboxApplication;
import no.nordicsemi.android.nrftoolbox.profile.BleProfileService;
import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager;
public class RSCService extends BleProfileService implements RSCManagerCallbacks {
private static final String TAG = "RSCService";
public static final String BROADCAST_RSC_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_RSC_MEASUREMENT";
public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_SPEED";
public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_CADENCE";
public static final String EXTRA_STRIDE_LENGTH = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDE_LENGTH";
public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_TOTAL_DISTANCE";
public static final String EXTRA_ACTIVITY = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_ACTIVITY";
public static final String BROADCAST_STRIDES_UPDATE = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_STRIDES_UPDATE";
public static final String EXTRA_STRIDES = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDES";
public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_DISTANCE";
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.rsc.ACTION_DISCONNECT";
private RSCManager mManager;
/** The last value of a cadence */
private float mCadence;
/** Trip distance in cm */
private long mDistance;
/** Stride length in cm */
private Integer mStrideLength;
/** Number of steps in the trip */
private int mStepsNumber;
private boolean mTaskInProgress;
private final Handler mHandler = new Handler();
private final static int NOTIFICATION_ID = 200;
private final static int OPEN_ACTIVITY_REQ = 0;
private final static int DISCONNECT_REQ = 1;
private final LocalBinder mBinder = new RSCBinder();
/**
* This local binder is an interface for the bound activity to operate with the RSC sensor.
*/
class RSCBinder extends LocalBinder {
// empty
}
@Override
protected LocalBinder getBinder() {
return mBinder;
}
@Override
protected LoggableBleManager<RSCManagerCallbacks> initializeManager() {
return mManager = new RSCManager(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();
if (isConnected()) {
// This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property).
// If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications.
mManager.readBatteryLevelCharacteristic();
}
}
@Override
protected void onUnbind() {
// When we are connected, but the application is not open, we are not really interested in battery level notifications.
// But we will still be receiving other values, if enabled.
if (isConnected())
mManager.disableBatteryLevelCharacteristicNotifications();
// when the activity closes we need to show the notification that user is connected to the sensor
createNotification(R.string.rsc_notification_connected_message, 0);
}
private final Runnable mUpdateStridesTask = new Runnable() {
@Override
public void run() {
if (!isConnected())
return;
mStepsNumber++;
mDistance += mStrideLength; // [cm]
final Intent broadcast = new Intent(BROADCAST_STRIDES_UPDATE);
broadcast.putExtra(EXTRA_STRIDES, mStepsNumber);
broadcast.putExtra(EXTRA_DISTANCE, mDistance);
LocalBroadcastManager.getInstance(RSCService.this).sendBroadcast(broadcast);
if (mCadence > 0) {
final long interval = (long) (1000.0f * 60.0f / mCadence);
mHandler.postDelayed(mUpdateStridesTask, interval);
} else {
mTaskInProgress = false;
}
}
};
@Override
public void onRSCMeasurementReceived(@NonNull final BluetoothDevice device, final boolean running,
final float instantaneousSpeed, final int instantaneousCadence,
@Nullable final Integer strideLength,
@Nullable final Long totalDistance) {
final Intent broadcast = new Intent(BROADCAST_RSC_MEASUREMENT);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_SPEED, instantaneousSpeed);
broadcast.putExtra(EXTRA_CADENCE, instantaneousCadence);
broadcast.putExtra(EXTRA_STRIDE_LENGTH, strideLength);
broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance);
broadcast.putExtra(EXTRA_ACTIVITY, running);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
// Start strides counter if not in progress
mCadence = instantaneousCadence;
if (strideLength != null) {
mStrideLength = strideLength;
}
if (!mTaskInProgress && strideLength != null && instantaneousCadence > 0) {
mTaskInProgress = true;
final long interval = (long) (1000.0f * 60.0f / mCadence);
mHandler.postDelayed(mUpdateStridesTask, interval);
}
}
@Override
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int value) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice());
broadcast.putExtra(EXTRA_BATTERY_LEVEL, value);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
/**
* Creates the notification
*
* @param messageResId
* message resource id. The message must have one String parameter,<br />
* f.e. <code>&lt;string name="name"&gt;%s is connected&lt;/string&gt;</code>
* @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, RSCActivity.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 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL);
builder.setContentIntent(pendingIntent);
builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName()));
builder.setSmallIcon(R.drawable.ic_stat_notify_rsc);
builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true);
builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.rsc_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();
}
};
}

View File

@@ -0,0 +1,56 @@
/*
* 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.rsc.settings;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Display the fragment as the main content.
getSupportFragmentManager().beginTransaction().replace(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);
}
}

View File

@@ -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.rsc.settings;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import androidx.preference.PreferenceFragmentCompat;
import no.nordicsemi.android.nrftoolbox.R;
public class SettingsFragment extends PreferenceFragmentCompat {
public static final String SETTINGS_UNIT = "settings_rsc_unit";
public static final int SETTINGS_UNIT_M_S = 0; // [m/s]
public static final int SETTINGS_UNIT_KM_H = 1; // [m/s]
public static final int SETTINGS_UNIT_MPH = 2; // [m/s]
public static final int SETTINGS_UNIT_DEFAULT = SETTINGS_UNIT_KM_H;
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.settings_rsc);
}
}

View File

@@ -0,0 +1,214 @@
/*
* 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.scanner;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.support.v18.scanner.ScanResult;
/**
* DeviceListAdapter class is list adapter for showing scanned Devices name, address and RSSI image based on RSSI values.
*/
public class DeviceListAdapter extends BaseAdapter {
private static final int TYPE_TITLE = 0;
private static final int TYPE_ITEM = 1;
private static final int TYPE_EMPTY = 2;
private final ArrayList<ExtendedBluetoothDevice> mListBondedValues = new ArrayList<>();
private final ArrayList<ExtendedBluetoothDevice> mListValues = new ArrayList<>();
private final Context mContext;
public DeviceListAdapter(final Context context) {
mContext = context;
}
/**
* Sets a list of bonded devices.
* @param devices list of bonded devices.
*/
public void addBondedDevices(final Set<BluetoothDevice> devices) {
final List<ExtendedBluetoothDevice> bondedDevices = mListBondedValues;
for (BluetoothDevice device : devices) {
bondedDevices.add(new ExtendedBluetoothDevice(device));
}
notifyDataSetChanged();
}
/**
* Updates the list of not bonded devices.
* @param results list of results from the scanner
*/
public void update(final List<ScanResult> results) {
for (final ScanResult result : results) {
final ExtendedBluetoothDevice device = findDevice(result);
if (device == null) {
mListValues.add(new ExtendedBluetoothDevice(result));
} else {
device.name = result.getScanRecord() != null ? result.getScanRecord().getDeviceName() : null;
device.rssi = result.getRssi();
}
}
notifyDataSetChanged();
}
private ExtendedBluetoothDevice findDevice(final ScanResult result) {
for (final ExtendedBluetoothDevice device : mListBondedValues)
if (device.matches(result))
return device;
for (final ExtendedBluetoothDevice device : mListValues)
if (device.matches(result))
return device;
return null;
}
public void clearDevices() {
mListValues.clear();
notifyDataSetChanged();
}
@Override
public int getCount() {
final int bondedCount = mListBondedValues.size() + 1; // 1 for the title
final int availableCount = mListValues.isEmpty() ? 2 : mListValues.size() + 1; // 1 for title, 1 for empty text
if (bondedCount == 1)
return availableCount;
return bondedCount + availableCount;
}
@Override
public Object getItem(int position) {
final int bondedCount = mListBondedValues.size() + 1; // 1 for the title
if (mListBondedValues.isEmpty()) {
if (position == 0)
return R.string.scanner_subtitle_not_bonded;
else
return mListValues.get(position - 1);
} else {
if (position == 0)
return R.string.scanner_subtitle_bonded;
if (position < bondedCount)
return mListBondedValues.get(position - 1);
if (position == bondedCount)
return R.string.scanner_subtitle_not_bonded;
return mListValues.get(position - bondedCount - 1);
}
}
@Override
public int getViewTypeCount() {
return 3;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
return getItemViewType(position) == TYPE_ITEM;
}
@Override
public int getItemViewType(int position) {
if (position == 0)
return TYPE_TITLE;
if (!mListBondedValues.isEmpty() && position == mListBondedValues.size() + 1)
return TYPE_TITLE;
if (position == getCount() - 1 && mListValues.isEmpty())
return TYPE_EMPTY;
return TYPE_ITEM;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View oldView, ViewGroup parent) {
final LayoutInflater inflater = LayoutInflater.from(mContext);
final int type = getItemViewType(position);
View view = oldView;
switch (type) {
case TYPE_EMPTY:
if (view == null) {
view = inflater.inflate(R.layout.device_list_empty, parent, false);
}
break;
case TYPE_TITLE:
if (view == null) {
view = inflater.inflate(R.layout.device_list_title, parent, false);
}
final TextView title = (TextView) view;
title.setText((Integer) getItem(position));
break;
default:
if (view == null) {
view = inflater.inflate(R.layout.device_list_row, parent, false);
final ViewHolder holder = new ViewHolder();
holder.name = view.findViewById(R.id.name);
holder.address = view.findViewById(R.id.address);
holder.rssi = view.findViewById(R.id.rssi);
view.setTag(holder);
}
final ExtendedBluetoothDevice device = (ExtendedBluetoothDevice) getItem(position);
final ViewHolder holder = (ViewHolder) view.getTag();
final String name = device.name;
holder.name.setText(name != null ? name : mContext.getString(R.string.not_available));
holder.address.setText(device.device.getAddress());
if (!device.isBonded || device.rssi != ExtendedBluetoothDevice.NO_RSSI) {
final int rssiPercent = (int) (100.0f * (127.0f + device.rssi) / (127.0f + 20.0f));
holder.rssi.setImageLevel(rssiPercent);
holder.rssi.setVisibility(View.VISIBLE);
} else {
holder.rssi.setVisibility(View.GONE);
}
break;
}
return view;
}
private class ViewHolder {
private TextView name;
private TextView address;
private ImageView rssi;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.scanner;
import android.bluetooth.BluetoothDevice;
import no.nordicsemi.android.support.v18.scanner.ScanResult;
public class ExtendedBluetoothDevice {
/* package */ static final int NO_RSSI = -1000;
public final BluetoothDevice device;
/** The name is not parsed by some Android devices, f.e. Sony Xperia Z1 with Android 4.3 (C6903). It needs to be parsed manually. */
public String name;
public int rssi;
public boolean isBonded;
public ExtendedBluetoothDevice(final ScanResult scanResult) {
this.device = scanResult.getDevice();
this.name = scanResult.getScanRecord() != null ? scanResult.getScanRecord().getDeviceName() : null;
this.rssi = scanResult.getRssi();
this.isBonded = false;
}
public ExtendedBluetoothDevice(final BluetoothDevice device) {
this.device = device;
this.name = device.getName();
this.rssi = NO_RSSI;
this.isBonded = true;
}
public boolean matches(final ScanResult scanResult) {
return device.getAddress().equals(scanResult.getDevice().getAddress());
}
}

View File

@@ -0,0 +1,290 @@
/*
* 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.scanner;
import android.Manifest;
import android.app.Dialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.DialogFragment;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat;
import no.nordicsemi.android.support.v18.scanner.ScanCallback;
import no.nordicsemi.android.support.v18.scanner.ScanFilter;
import no.nordicsemi.android.support.v18.scanner.ScanResult;
import no.nordicsemi.android.support.v18.scanner.ScanSettings;
/**
* ScannerFragment class scan required BLE devices and shows them in a list. This class scans and filter
* devices with standard BLE Service UUID and devices with custom BLE Service UUID. It contains a
* list and a button to scan/cancel. There is a interface {@link OnDeviceSelectedListener} which is
* implemented by activity in order to receive selected device. The scanning will continue to scan
* for 5 seconds and then stop.
*/
public class ScannerFragment extends DialogFragment {
private final static String TAG = "ScannerFragment";
private final static String PARAM_UUID = "param_uuid";
private final static long SCAN_DURATION = 5000;
private final static int REQUEST_PERMISSION_REQ_CODE = 34; // any 8-bit number
private BluetoothAdapter mBluetoothAdapter;
private OnDeviceSelectedListener mListener;
private DeviceListAdapter mAdapter;
private final Handler mHandler = new Handler();
private Button mScanButton;
private View mPermissionRationale;
private ParcelUuid mUuid;
private boolean mIsScanning = false;
public static ScannerFragment getInstance(final UUID uuid) {
final ScannerFragment fragment = new ScannerFragment();
final Bundle args = new Bundle();
if (uuid != null)
args.putParcelable(PARAM_UUID, new ParcelUuid(uuid));
fragment.setArguments(args);
return fragment;
}
/**
* Interface required to be implemented by activity.
*/
public interface OnDeviceSelectedListener {
/**
* Fired when user selected the device.
*
* @param device
* the device to connect to
* @param name
* the device name. Unfortunately on some devices {@link BluetoothDevice#getName()}
* always returns <code>null</code>, i.e. Sony Xperia Z1 (C6903) with Android 4.3.
* The name has to be parsed manually form the Advertisement packet.
*/
void onDeviceSelected(final BluetoothDevice device, final String name);
/**
* Fired when scanner dialog has been cancelled without selecting a device.
*/
void onDialogCanceled();
}
/**
* This will make sure that {@link OnDeviceSelectedListener} interface is implemented by activity.
*/
@Override
public void onAttach(final Context context) {
super.onAttach(context);
try {
this.mListener = (OnDeviceSelectedListener) context;
} catch (final ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement OnDeviceSelectedListener");
}
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle args = getArguments();
if (args != null && args.containsKey(PARAM_UUID)) {
mUuid = args.getParcelable(PARAM_UUID);
}
final BluetoothManager manager = (BluetoothManager) requireContext().getSystemService(Context.BLUETOOTH_SERVICE);
if (manager != null) {
mBluetoothAdapter = manager.getAdapter();
}
}
@Override
public void onDestroyView() {
stopScan();
super.onDestroyView();
}
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
final View dialogView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_device_selection, null);
final ListView listview = dialogView.findViewById(android.R.id.list);
listview.setEmptyView(dialogView.findViewById(android.R.id.empty));
listview.setAdapter(mAdapter = new DeviceListAdapter(getActivity()));
builder.setTitle(R.string.scanner_title);
final AlertDialog dialog = builder.setView(dialogView).create();
listview.setOnItemClickListener((parent, view, position, id) -> {
stopScan();
dialog.dismiss();
final ExtendedBluetoothDevice d = (ExtendedBluetoothDevice) mAdapter.getItem(position);
mListener.onDeviceSelected(d.device, d.name);
});
mPermissionRationale = dialogView.findViewById(R.id.permission_rationale); // this is not null only on API23+
mScanButton = dialogView.findViewById(R.id.action_cancel);
mScanButton.setOnClickListener(v -> {
if (v.getId() == R.id.action_cancel) {
if (mIsScanning) {
dialog.cancel();
} else {
startScan();
}
}
});
addBoundDevices();
if (savedInstanceState == null)
startScan();
return dialog;
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
mListener.onDialogCanceled();
}
@Override
public void onRequestPermissionsResult(final int requestCode, final @NonNull String[] permissions, final @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION_REQ_CODE: {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// We have been granted the Manifest.permission.ACCESS_COARSE_LOCATION permission. Now we may proceed with scanning.
startScan();
} else {
mPermissionRationale.setVisibility(View.VISIBLE);
Toast.makeText(getActivity(), R.string.no_required_permission, Toast.LENGTH_SHORT).show();
}
break;
}
}
}
/**
* Scan for 5 seconds and then stop scanning when a BluetoothLE device is found then mLEScanCallback
* is activated This will perform regular scan for custom BLE Service UUID and then filter out.
* using class ScannerServiceParser
*/
private void startScan() {
// Since Android 6.0 we need to obtain either Manifest.permission.ACCESS_COARSE_LOCATION or Manifest.permission.ACCESS_FINE_LOCATION to be able to scan for
// Bluetooth LE devices. This is related to beacons as proximity devices.
// On API older than Marshmallow the following code does nothing.
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// When user pressed Deny and still wants to use this functionality, show the rationale
if (ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) && mPermissionRationale.getVisibility() == View.GONE) {
mPermissionRationale.setVisibility(View.VISIBLE);
return;
}
requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_PERMISSION_REQ_CODE);
return;
}
// Hide the rationale message, we don't need it anymore.
if (mPermissionRationale != null)
mPermissionRationale.setVisibility(View.GONE);
mAdapter.clearDevices();
mScanButton.setText(R.string.scanner_action_cancel);
final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
final ScanSettings settings = new ScanSettings.Builder()
.setLegacy(false)
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(1000).setUseHardwareBatchingIfSupported(false).build();
final List<ScanFilter> filters = new ArrayList<>();
filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build());
scanner.startScan(filters, settings, scanCallback);
mIsScanning = true;
mHandler.postDelayed(() -> {
if (mIsScanning) {
stopScan();
}
}, SCAN_DURATION);
}
/**
* Stop scan if user tap Cancel button
*/
private void stopScan() {
if (mIsScanning) {
mScanButton.setText(R.string.scanner_action_scan);
final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
scanner.stopScan(scanCallback);
mIsScanning = false;
}
}
private ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(final int callbackType, final ScanResult result) {
// do nothing
}
@Override
public void onBatchScanResults(final List<ScanResult> results) {
mAdapter.update(results);
}
@Override
public void onScanFailed(final int errorCode) {
// should never be called
}
};
private void addBoundDevices() {
final Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
mAdapter.addBondedDevices(devices);
}
}

View File

@@ -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:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="no.nordicsemi.android.nrftoolbox.LAUNCHER" />
</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.

View File

@@ -0,0 +1,226 @@
/*
* 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.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.view.Menu;
import android.widget.EditText;
import android.widget.Switch;
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<TemplateService.TemplateBinder> {
@SuppressWarnings("unused")
private final String TAG = "TemplateActivity";
// TODO change view references to match your need
private TextView mValueView;
private TextView mHeatingStatusView;
private EditText mTimeValueView;
private EditText mTempValueView;
private Integer mSetTime ;
private Integer mSetTemp ;
private Switch mHeatingState ;
@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 = findViewById(R.id.value);
mTimeValueView = findViewById(R.id.u_time);
mTempValueView = findViewById(R.id.u_temp);
mHeatingState = findViewById(R.id.heating_state);
mHeatingStatusView= findViewById(R.id.heating_status_text);
mHeatingState.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
getService().heatingSwitch(1);
mSetTime = Integer.parseInt(mTimeValueView.getText().toString());
getService().sendTimeDuration(mSetTime);
mSetTemp = Integer.parseInt(mTempValueView.getText().toString());
getService().sendTemp(mSetTemp);
}
else {
getService().heatingSwitch(0);
}
mHeatingStatusView.setText(isChecked ? R.string.Heating_block_on : R.string.Heating_block_off);
});
findViewById(R.id.time_set_button).setOnClickListener(v -> {
if (isDeviceConnected()) {
mSetTime = Integer.parseInt(mTimeValueView.getText().toString());
getService().sendTimeDuration(mSetTime);
}
});
findViewById(R.id.temp_set_button).setOnClickListener(v -> {
if (isDeviceConnected()) {
mSetTemp = Integer.parseInt(mTempValueView.getText().toString());
getService().sendTemp(mSetTemp);
}
});
}
@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 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<? extends BleProfileService> getServiceClass() {
return TemplateService.class;
}
@Override
protected void onServiceBound(final TemplateService.TemplateBinder binder) {
// not used
}
@Override
protected void onServiceUnbound() {
// not used
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
// this may notify user or show some views
}
@Override
public void onDeviceDisconnected(final BluetoothDevice device) {
super.onDeviceDisconnected(device);
}
// Handling updates from the device
@SuppressWarnings("unused")
private void setValueOnView(@NonNull final BluetoothDevice device, final int value) {
// TODO assign the value to a view
mValueView.setText(String.valueOf(value));
}
@SuppressWarnings("unused")
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int value) {
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
final BluetoothDevice device = intent.getParcelableExtra(TemplateService.EXTRA_DEVICE);
if (TemplateService.BROADCAST_TEMPLATE_MEASUREMENT.equals(action)) {
final int value = intent.getIntExtra(TemplateService.EXTRA_DATA, 0);
// Update GUI
setValueOnView(device, value);
} else if (TemplateService.BROADCAST_BATTERY_LEVEL.equals(action)) {
final int batteryLevel = intent.getIntExtra(TemplateService.EXTRA_BATTERY_LEVEL, 0);
// Update GUI
onBatteryLevelChanged(device, batteryLevel);
}
}
};
private static IntentFilter makeIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TemplateService.BROADCAST_TEMPLATE_MEASUREMENT);
intentFilter.addAction(TemplateService.BROADCAST_BATTERY_LEVEL);
return intentFilter;
}
}

View File

@@ -0,0 +1,283 @@
/*
* 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.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.TemplateParser;
import no.nordicsemi.android.nrftoolbox.template.callback.TemplateDataCallback;
/**
* Modify to template manager to match your requirements.
* The TemplateManager extends {@link BatteryManager}, but it may easily extend {@link BleManager}
* instead if you don't need Battery Service support. If not, also modify the
* {@link TemplateManagerCallbacks} to extend {@link no.nordicsemi.android.ble.BleManagerCallbacks}
* and replace BatteryManagerGattCallback to BleManagerGattCallback in this class.
*/
public class TemplateManager extends BatteryManager<TemplateManagerCallbacks> {
// TODO Replace the services and characteristics below to match your device.
/**
* The service UUID.
*/
static final UUID SERVICE_UUID = UUID.fromString("00001500-1212-efde-1523-785feabcd123"); // Heart Rate service
/**
* A UUID of a characteristic with notify property.
*/
//private static final UUID MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00001501-1212-efde-1523-785feabcd123"); // Heart Rate Measurement
//private static final UUID TEMPERATURE_CHARACTERISTIC_UUID = UUID.fromString("00001501-1212-efde-1523-785feabcd123"); // Heart Rate Measurement
/**
* A UUID of a characteristic with read property.
*/
//private static final UUID READABLE_CHARACTERISTIC_UUID = UUID.fromString("00001502-1212-efde-1523-785feabcd123"); // Body Sensor Location
private static final UUID TEMP_READABLE_CHARACTERISTIC_UUID = UUID.fromString("00001501-1212-efde-1523-785feabcd123"); // Body Sensor Location
/**
* A UUID of a characteristic with write property.
*/
private static final UUID TIME_WRITABLE_CHARACTERISTIC_UUID = UUID.fromString("00001502-1212-efde-1523-785feabcd123"); // Device Name
/**
* A UUID of a characteristic with write property.
*/
private static final UUID TEMP_WRITABLE_CHARACTERISTIC_UUID = UUID.fromString("00001503-1212-efde-1523-785feabcd123"); // Device Name
/**
* A UUID of a characteristic with write property.
*/
private static final UUID AMP_WRITABLE_CHARACTERISTIC_UUID = UUID.fromString("00001504-1212-efde-1523-785feabcd123"); // Device Name
/**
* Some other service UUID.
*/
private static final UUID OTHER_SERVICE_UUID = UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"); // Generic Access service
/**
* A UUID of a characteristic with write property.
*/
private static final UUID READABLE_CHARACTERISTIC_UUID = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb"); // Device Name
// TODO Add more services and characteristics references.
private BluetoothGattCharacteristic mReadBlockTempCharacteristic, mSetAmpTempCharacteristic, mSetAmpTimeCharacteristic, mAmpStartStopCharacteristic, mRequiredCharacteristic, mDeviceNameCharacteristic, mOptionalCharacteristic;
public TemplateManager(final Context context) {
super(context);
}
@NonNull
@Override
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc.
*/
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected void initialize() {
// Initialize the Battery Manager. It will enable Battery Level notifications.
// Remove it if you don't need this feature.
super.initialize();
// TODO Initialize your manager here.
// Initialization is done once, after the device is connected. Usually it should
// enable notifications or indications on some characteristics, write some data or
// read some features / version.
// After the initialization is complete, the onDeviceReady(...) method will be called.
// Increase the MTU
requestMtu(43)//requestMtu(43)
.with((device, mtu) -> log(LogContract.Log.Level.APPLICATION, "MTU changed to " + mtu))
.done(device -> {
// You may do some logic in here that should be done when the request finished successfully.
// In case of MTU this method is called also when the MTU hasn't changed, or has changed
// to a different (lower) value. Use .with(...) to get the MTU value.
})
.fail((device, status) -> log(Log.WARN, "MTU change not supported"))
.enqueue();
// Set notification callback
setNotificationCallback(mReadBlockTempCharacteristic)
// This callback will be called each time the notification is received
.with(new TemplateDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, TemplateParser.parse(data));
super.onDataReceived(device, data);
}
@Override
public void onSampleValueReceived(@NonNull final BluetoothDevice device, final int value) {
// Let's lass received data to the service
mCallbacks.onSampleValueReceived(device, value);
}
@Override
public void onInvalidDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(Log.WARN, "Invalid data received: " + data);
}
});
// Enable notifications
enableNotifications(mReadBlockTempCharacteristic)
// Method called after the data were sent (data will contain 0x0100 in this case)
.with((device, data) -> log(Log.DEBUG, "Data sent: " + data))
// Method called when the request finished successfully. This will be called after .with(..) callback
.done(device -> log(LogContract.Log.Level.APPLICATION, "Notifications enabled successfully"))
// Methods called in case of an error, for example when the characteristic does not have Notify property
.fail((device, status) -> log(Log.WARN, "Failed to enable notifications"))
.enqueue();
}
@Override
protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
// TODO Initialize required characteristics.
// It should return true if all has been discovered (that is that device is supported).
final BluetoothGattService service = gatt.getService(SERVICE_UUID);
if (service != null) {
mReadBlockTempCharacteristic = service.getCharacteristic(TEMP_READABLE_CHARACTERISTIC_UUID);
mSetAmpTimeCharacteristic = service.getCharacteristic(TIME_WRITABLE_CHARACTERISTIC_UUID);
mSetAmpTempCharacteristic = service.getCharacteristic(TEMP_WRITABLE_CHARACTERISTIC_UUID);
mAmpStartStopCharacteristic = service.getCharacteristic(AMP_WRITABLE_CHARACTERISTIC_UUID);
}
final BluetoothGattService otherService = gatt.getService(OTHER_SERVICE_UUID);
if (otherService != null) {
mDeviceNameCharacteristic = otherService.getCharacteristic(READABLE_CHARACTERISTIC_UUID);
}
return mReadBlockTempCharacteristic != null && mSetAmpTimeCharacteristic != null && mSetAmpTempCharacteristic!= null && mAmpStartStopCharacteristic != null ;
}
@Override
protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
// Initialize Battery characteristic
super.isOptionalServiceSupported(gatt);
// TODO If there are some optional characteristics, initialize them there.
final BluetoothGattService service = gatt.getService(SERVICE_UUID);
if (service != null) {
mOptionalCharacteristic = service.getCharacteristic(READABLE_CHARACTERISTIC_UUID);
}
return mOptionalCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
// Release Battery Service
super.onDeviceDisconnected();
// TODO Release references to your characteristics.
mRequiredCharacteristic = null;
mDeviceNameCharacteristic = null;
mOptionalCharacteristic = null;
}
@Override
protected void onDeviceReady() {
super.onDeviceReady();
// Initialization is now ready.
// The service or activity has been notified with no.nordicsemi.android.ble.BatteryManagerCallbacks#onDeviceReady().
// TODO Do some extra logic here, of remove onDeviceReady().
// Device is ready, let's read something here. Usually there is nothing else to be done
// here, as all had been done during initialization.
readCharacteristic(mReadBlockTempCharacteristic)
.with((device, data) -> {
// Characteristic value has been read
// Let's do some magic with it.
if (data.size() > 0) {
final Integer value = data.getIntValue(Data.FORMAT_UINT8, 0);
log(LogContract.Log.Level.APPLICATION, "Value '" + value + "' has been read!");
} else {
log(Log.WARN, "Value is empty!");
}
})
.enqueue();
}
};
// TODO Define manager's API
/**
* This method will write important data to the device.
*
* @param parameter parameter to be written.
*/
//Heating Switch
void heatingSwitch(int state) {
log(Log.WARN, "Write time duration \"" + state + "\"");
//final byte time_in_byte = time_duration.getBytes()[0];
final byte state_in_byte = (byte) state;
writeCharacteristic(mAmpStartStopCharacteristic, Data.opCode(state_in_byte))
.with((device, data) -> log(LogContract.Log.Level.APPLICATION,
"\"" + data.size() + "\" Heating State"))
.split()
.enqueue();
}
//Set Time
void sendTimeDuration(int time_duration) {
log(Log.WARN, "Write time duration \"" + time_duration + "\"");
//final byte time_in_byte = time_duration.getBytes()[0];
final byte time_in_byte = (byte) time_duration;
writeCharacteristic(mSetAmpTimeCharacteristic, Data.opCode(time_in_byte))
.with((device, data) -> log(LogContract.Log.Level.APPLICATION,
"\"" + data.size() + "\" Set Time"))
.split()
.enqueue();
}
//Set Temp
void sendTemp(int temp) {
log(Log.WARN, "Write temp \"" + temp + "\"");
//final byte temp_in_byte = temp.getBytes()[0];
final byte temp_in_byte = (byte) temp;
writeCharacteristic(mSetAmpTempCharacteristic, Data.opCode(temp_in_byte))
.with((device, data) -> log(LogContract.Log.Level.APPLICATION,
"\"" + data.size() + "\" Set Temp"))
.split()
.enqueue();
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.battery.BatteryManagerCallbacks;
import no.nordicsemi.android.nrftoolbox.template.callback.TemplateCharacteristicCallback;
/**
* Interface {@link TemplateManagerCallbacks} must be implemented by {@link TemplateService}
* in order to receive callbacks from {@link TemplateManager}
*/
interface TemplateManagerCallbacks extends BatteryManagerCallbacks, TemplateCharacteristicCallback {
// 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.
// TODO add more callbacks.
// If you need, create more ...Callback interfaces and extend this interface with them.
}

Some files were not shown because too many files have changed in this diff Show More