diff --git a/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js
index df85886da..caf39669f 100644
--- a/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js
+++ b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js
@@ -1,3 +1,21 @@
+if (window.Storage){
+ var prefix = GBjs.getAppLocalstoragePrefix();
+ GBjs.gbLog("redefining local storage with prefix: " + prefix);
+
+ Storage.prototype.setItem = (function(key, value) {
+ this.call(localStorage,prefix + key, value);
+ }).bind(Storage.prototype.setItem);
+
+ Storage.prototype.getItem = (function(key) {
+// console.log("I am about to return " + prefix + key);
+ var def = null;
+ if(key == 'clay-settings') {
+ def = '{}';
+ }
+ return this.call(localStorage,prefix + key) || def;
+ }).bind(Storage.prototype.getItem);
+}
+
function loadScript(url, callback) {
// Adding the script tag to the head as suggested before
var head = document.getElementsByTagName('head')[0];
@@ -29,45 +47,78 @@ function getURLVariable(variable, defaultValue) {
return defaultValue || false;
}
+function showStep(desiredStep) {
+ var steps = document.getElementsByClassName("step");
+ var testStep = null;
+ for (var i = 0; i < steps.length; i ++) {
+ if (steps[i].id == desiredStep)
+ testStep = steps[i].id;
+ }
+ if (testStep !== null) {
+ for (var i = 0; i < steps.length; i ++) {
+ steps[i].style.display = 'none';
+ }
+ document.getElementById(desiredStep).style.display="block";
+ }
+}
+
function gbPebble() {
this.configurationURL = null;
this.configurationValues = null;
+ var self = this;
+ self.events = {};
+ //events processing: see http://stackoverflow.com/questions/10978311/implementing-events-in-my-own-object
+ self.addEventListener = function(name, handler) {
+ if (self.events.hasOwnProperty(name))
+ self.events[name].push(handler);
+ else
+ self.events[name] = [handler];
+ }
- this.addEventListener = function(e, f) {
- if(e == 'ready') {
- this.ready = f;
- }
- if(e == 'showConfiguration') {
- this.showConfiguration = f;
- }
- if(e == 'webviewclosed') {
- this.parseconfig = f;
- }
- if(e == 'appmessage') {
- this.appmessage = f;
+ self.removeEventListener = function(name, handler) {
+ if (!self.events.hasOwnProperty(name))
+ return;
+
+ var index = self.events[name].indexOf(handler);
+ if (index != -1)
+ self.events[name].splice(index, 1);
+ }
+
+ self.evaluate = function(name, args) {
+ if (!self.events.hasOwnProperty(name))
+ return;
+
+ if (!args || !args.length)
+ args = [];
+
+ var evs = self.events[name], l = evs.length;
+ for (var i = 0; i < l; i++) {
+ evs[i].apply(null, args);
}
}
- this.removeEventListener = function(e, f) {
- if(e == 'ready') {
- this.ready = null;
- }
- if(e == 'showConfiguration') {
- this.showConfiguration = null;
- }
- if(e == 'webviewclosed') {
- this.parseconfig = null;
- }
- if(e == 'appmessage') {
- this.appmessage = null;
- }
- }
this.actuallyOpenURL = function() {
- window.open(this.configurationURL.toString(), "config");
+ showStep("step1compat");
+ window.open(self.configurationURL.toString(), "config");
}
this.actuallySendData = function() {
- GBjs.sendAppMessage(this.configurationValues);
+ GBjs.sendAppMessage(self.configurationValues);
+ GBjs.closeActivity();
+ }
+
+ this.savePreset = function() {
+ GBjs.saveAppStoredPreset(self.configurationValues);
+ }
+
+ this.loadPreset = function() {
+ showStep("step2");
+ var presetElements = document.getElementsByClassName("store_presets");
+ for (var i = 0; i < presetElements.length; i ++) {
+ presetElements[i].style.display = 'none';
+ }
+ self.configurationValues = GBjs.getAppStoredPreset();
+ document.getElementById("jsondata").innerHTML=self.configurationValues;
}
//needs to be called like this because of original Pebble function name
@@ -75,7 +126,7 @@ function gbPebble() {
if (url.lastIndexOf("http", 0) === 0) {
document.getElementById("config_url").innerHTML=url;
var UUID = GBjs.getAppUUID();
- this.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
+ self.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
} else {
//TODO: add custom return_to
location.href = url;
@@ -89,8 +140,8 @@ function gbPebble() {
this.sendAppMessage = function (dict, callbackAck, callbackNack){
try {
- this.configurationValues = JSON.stringify(dict);
- document.getElementById("jsondata").innerHTML=this.configurationValues;
+ self.configurationValues = JSON.stringify(dict);
+ document.getElementById("jsondata").innerHTML=self.configurationValues;
return callbackAck;
}
catch (e) {
@@ -107,31 +158,62 @@ function gbPebble() {
return GBjs.getWatchToken();
}
+ this.getTimelineToken = function() {
+ return '';
+ }
+
this.showSimpleNotificationOnPebble = function(title, body) {
GBjs.gbLog("app wanted to show: " + title + " body: "+ body);
}
- this.ready = function() {
+
+ this.showConfiguration = function() {
+ console.error("This watchapp doesn't support configuration");
+ GBjs.closeActivity();
}
+ this.parseReturnedPebbleJS = function() {
+ var str = document.getElementById('pastereturn').value;
+ var needle = "pebblejs://close#";
+
+ if (str.split(needle)[1] !== undefined) {
+ var t = new Object();
+ t.response = decodeURIComponent(str.split(needle)[1]);
+ self.evaluate('webviewclosed',[t]);
+ showStep("step2");
+ } else {
+ console.error("No valid configuration found in the entered string.");
+ }
+ }
}
var Pebble = new gbPebble();
var jsConfigFile = GBjs.getAppConfigurationFile();
+var storedPreset = GBjs.getAppStoredPreset();
+
+document.addEventListener('DOMContentLoaded', function(){
if (jsConfigFile != null) {
loadScript(jsConfigFile, function() {
+ Pebble.evaluate('ready');
if (getURLVariable('config') == 'true') {
- document.getElementById('step1').style.display="none";
- var json_string = unescape(getURLVariable('json'));
+ showStep("step2");
+ var json_string = getURLVariable('json');
var t = new Object();
t.response = json_string;
- if (json_string != '')
- Pebble.parseconfig(t);
+ if (json_string != '') {
+ Pebble.evaluate('webviewclosed',[t]);
+ }
+
} else {
- document.getElementById('step2').style.display="none";
- Pebble.ready();
- Pebble.showConfiguration();
+ if (storedPreset === undefined) {
+ var presetElements = document.getElementsByClassName("load_presets");
+ for (var i = 0; i < presetElements.length; i ++) {
+ presetElements[i].style.display = 'none';
+ }
+ }
+ Pebble.evaluate('showConfiguration');
}
});
}
+}, false);
\ No newline at end of file
diff --git a/app/src/main/assets/devintro.png b/app/src/main/assets/devintro.png
index 6fc1a5935..859cfdeee 100644
Binary files a/app/src/main/assets/devintro.png and b/app/src/main/assets/devintro.png differ
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java
index 0113effbb..b06593832 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java
@@ -4,13 +4,12 @@ import android.annotation.TargetApi;
import android.app.Application;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
-import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
@@ -23,13 +22,18 @@ import android.util.TypedValue;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBConstants;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
@@ -39,8 +43,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
-//import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
-
/**
* Main Application class that initializes and provides access to certain things like
* logging and DB access.
@@ -48,8 +50,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class GBApplication extends Application {
// Since this class must not log to slf4j, we use plain android.util.Log
private static final String TAG = "GBApplication";
+ public static final String DATABASE_NAME = "Gadgetbridge";
+
private static GBApplication context;
- private static ActivityDatabaseHandler mActivityDatabaseHandler;
private static final Lock dbLock = new ReentrantLock();
private static DeviceService deviceService;
private static SharedPreferences sharedPrefs;
@@ -59,6 +62,7 @@ public class GBApplication extends Application {
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
private static Prefs prefs;
private static GBPrefs gbPrefs;
+ private static LockHandler lockHandler;
/**
* Note: is null on Lollipop and Kitkat
*/
@@ -73,20 +77,14 @@ public class GBApplication extends Application {
return dir.getAbsolutePath();
}
};
- private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- switch (action) {
- case ACTION_QUIT:
- quit();
- break;
- }
- }
- };
- private void quit() {
- GB.removeAllNotifications(this);
+ private DeviceManager deviceManager;
+
+ public static void quit() {
+ GB.log("Quitting Gadgetbridge...", GB.INFO, null);
+ Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
+ GBApplication.deviceService().quit();
}
public GBApplication() {
@@ -102,6 +100,11 @@ public class GBApplication extends Application {
public void onCreate() {
super.onCreate();
+ if (lockHandler != null) {
+ // guard against multiple invocations (robolectric)
+ return;
+ }
+
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs = new Prefs(sharedPrefs);
gbPrefs = new GBPrefs(prefs);
@@ -116,22 +119,43 @@ public class GBApplication extends Application {
setupExceptionHandler();
- deviceService = createDeviceService();
GB.environment = GBEnvironment.createDeviceEnvironment();
- mActivityDatabaseHandler = new ActivityDatabaseHandler(context);
- loadBlackList();
- IntentFilter filterLocal = new IntentFilter();
- filterLocal.addAction(ACTION_QUIT);
- LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
+ setupDatabase(this);
+
+ deviceManager = new DeviceManager(this);
+
+ deviceService = createDeviceService();
+ loadBlackList();
if (isRunningMarshmallowOrLater()) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
+ }
-// for testing DB stuff
-// SQLiteDatabase db = mActivityDatabaseHandler.getWritableDatabase();
-// db.close();
+ @Override
+ public void onTrimMemory(int level) {
+ super.onTrimMemory(level);
+ if (level >= TRIM_MEMORY_BACKGROUND) {
+ if (!hasBusyDevice()) {
+ DBHelper.clearSession();
+ }
+ }
+ }
+
+ /**
+ * Returns true if at least a single device is busy, e.g synchronizing activity data
+ * or something similar.
+ * Note: busy is not the same as connected or initialized!
+ */
+ private boolean hasBusyDevice() {
+ List
devices = getDeviceManager().getDevices();
+ for (GBDevice device : devices) {
+ if (device.isBusy()) {
+ return true;
+ }
+ }
+ return false;
}
public static void setupLogging(boolean enabled) {
@@ -147,6 +171,16 @@ public class GBApplication extends Application {
return prefs.getBoolean("log_to_file", false);
}
+ static void setupDatabase(Context context) {
+ DBOpenHelper helper = new DBOpenHelper(context, DATABASE_NAME, null);
+ SQLiteDatabase db = helper.getWritableDatabase();
+ DaoMaster daoMaster = new DaoMaster(db);
+ if (lockHandler == null) {
+ lockHandler = new LockHandler();
+ }
+ lockHandler.init(daoMaster, helper);
+ }
+
public static Context getContext() {
return context;
}
@@ -166,6 +200,9 @@ public class GBApplication extends Application {
* when that was not successful
* If acquiring was successful, callers must call #releaseDB when they
* are done (from the same thread that acquired the lock!
+ *
+ * Callers must not hold a reference to the returned instance because it
+ * will be invalidated at some point.
*
* @return the DBHandler
* @throws GBException
@@ -174,7 +211,7 @@ public class GBApplication extends Application {
public static DBHandler acquireDB() throws GBException {
try {
if (dbLock.tryLock(30, TimeUnit.SECONDS)) {
- return mActivityDatabaseHandler;
+ return lockHandler;
}
} catch (InterruptedException ex) {
Log.i(TAG, "Interrupted while waiting for DB lock");
@@ -231,7 +268,7 @@ public class GBApplication extends Application {
@TargetApi(Build.VERSION_CODES.M)
public static boolean isPriorityNumber(int priorityType, String number) {
NotificationManager.Policy notificationPolicy = notificationManager.getNotificationPolicy();
- if(priorityType == Policy.PRIORITY_CATEGORY_MESSAGES) {
+ if (priorityType == Policy.PRIORITY_CATEGORY_MESSAGES) {
if ((notificationPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) == Policy.PRIORITY_CATEGORY_MESSAGES) {
return isPrioritySender(notificationPolicy.priorityMessageSenders, number);
}
@@ -285,17 +322,35 @@ public class GBApplication extends Application {
}
/**
- * Deletes the entire Activity database and recreates it with empty tables.
+ * Deletes both the old Activity database and the new one recreates it with empty tables.
*
* @return true on successful deletion
*/
- public static synchronized boolean deleteActivityDatabase() {
- if (mActivityDatabaseHandler != null) {
- mActivityDatabaseHandler.close();
- mActivityDatabaseHandler = null;
+ public static synchronized boolean deleteActivityDatabase(Context context) {
+ // TODO: flush, close, reopen db
+ if (lockHandler != null) {
+ lockHandler.closeDb();
+ }
+ DBHelper dbHelper = new DBHelper(context);
+ boolean result = true;
+ if (dbHelper.existsDB(DBConstants.DATABASE_NAME)) {
+ result = getContext().deleteDatabase(DBConstants.DATABASE_NAME);
+ }
+ result &= getContext().deleteDatabase(DATABASE_NAME);
+ return result;
+ }
+
+ /**
+ * Deletes the legacy (pre 0.12) Activity database
+ *
+ * @return true on successful deletion
+ */
+ public static synchronized boolean deleteOldActivityDatabase(Context context) {
+ DBHelper dbHelper = new DBHelper(context);
+ boolean result = true;
+ if (dbHelper.existsDB(DBConstants.DATABASE_NAME)) {
+ result = getContext().deleteDatabase(DBConstants.DATABASE_NAME);
}
- boolean result = getContext().deleteDatabase(DBConstants.DATABASE_NAME);
- mActivityDatabaseHandler = new ActivityDatabaseHandler(getContext());
return result;
}
@@ -365,6 +420,7 @@ public class GBApplication extends Application {
theme.resolveAttribute(android.R.attr.textColor, typedValue, true);
return typedValue.data;
}
+
public static int getBackgroundColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
@@ -379,4 +435,8 @@ public class GBApplication extends Application {
public static GBPrefs getGBPrefs() {
return gbPrefs;
}
+
+ public DeviceManager getDeviceManager() {
+ return deviceManager;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java
index df07dc46e..e30e7c5a9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java
@@ -13,9 +13,8 @@ public class GBEnvironment {
return env;
}
- public static GBEnvironment createDeviceEnvironment() {
- GBEnvironment env = new GBEnvironment();
- return env;
+ static GBEnvironment createDeviceEnvironment() {
+ return new GBEnvironment();
}
public final boolean isTest() {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LockHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LockHandler.java
new file mode 100644
index 000000000..979b34933
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LockHandler.java
@@ -0,0 +1,101 @@
+package nodomain.freeyourgadget.gadgetbridge;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+
+/**
+ * Provides lowlevel access to the database.
+ */
+public class LockHandler implements DBHandler {
+
+ private DaoMaster daoMaster = null;
+ private DaoSession session = null;
+ private SQLiteOpenHelper helper = null;
+
+ public LockHandler() {
+ }
+
+ public void init(DaoMaster daoMaster, DBOpenHelper helper) {
+ if (isValid()) {
+ throw new IllegalStateException("DB must be closed before initializing it again");
+ }
+ if (daoMaster == null) {
+ throw new IllegalArgumentException("daoMaster must not be null");
+ }
+ if (helper == null) {
+ throw new IllegalArgumentException("helper must not be null");
+ }
+ this.daoMaster = daoMaster;
+ this.helper = helper;
+
+ session = daoMaster.newSession();
+ if (session == null) {
+ throw new RuntimeException("Unable to create database session");
+ }
+ }
+
+ @Override
+ public DaoMaster getDaoMaster() {
+ return daoMaster;
+ }
+
+ private boolean isValid() {
+ return daoMaster != null;
+ }
+
+ private void ensureValid() {
+ if (!isValid()) {
+ throw new IllegalStateException("LockHandler is not in a valid state");
+ }
+ }
+
+ @Override
+ public void close() {
+ ensureValid();
+ GBApplication.releaseDB();
+ }
+
+ @Override
+ public synchronized void openDb() {
+ if (session != null) {
+ throw new IllegalStateException("session must be null");
+ }
+ // this will create completely new db instances and in turn update this handler through #init()
+ GBApplication.setupDatabase(GBApplication.getContext());
+ }
+
+ @Override
+ public synchronized void closeDb() {
+ if (session == null) {
+ throw new IllegalStateException("session must not be null");
+ }
+ session.clear();
+ session.getDatabase().close();
+ session = null;
+ helper = null;
+ daoMaster = null;
+ }
+
+ @Override
+ public SQLiteOpenHelper getHelper() {
+ ensureValid();
+ return helper;
+ }
+
+ @Override
+ public DaoSession getDaoSession() {
+ ensureValid();
+ return session;
+ }
+
+ @Override
+ public SQLiteDatabase getDatabase() {
+ ensureValid();
+ return daoMaster.getDatabase();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java
index 7ea3c8e90..3d71b91fb 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java
@@ -125,4 +125,24 @@ public abstract class Logging {
}
return false;
}
+
+ public static String formatBytes(byte[] bytes) {
+ if (bytes == null) {
+ return "(null)";
+ }
+ StringBuilder builder = new StringBuilder(bytes.length * 5);
+ for (byte b : bytes) {
+ builder.append(String.format("0x%2x", b));
+ builder.append(" ");
+ }
+ return builder.toString().trim();
+ }
+
+ public static void logBytes(Logger logger, byte[] value) {
+ if (value != null) {
+ for (byte b : value) {
+ logger.warn("DATA: " + String.format("0x%2x", b));
+ }
+ }
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java
index e501de497..1c8ed55ad 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java
@@ -17,13 +17,12 @@ import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
-import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* Implementation of SleepAlarmWidget functionality. When pressing the widget, an alarm will be set
* to trigger after a predefined number of hours. A toast will confirm the user about this. The
- * value is retrieved using ActivityUser.().getActivityUserSleepDuration().
+ * value is retrieved using ActivityUser.().getSleepDuration().
*/
public class SleepAlarmWidget extends AppWidgetProvider {
@@ -71,23 +70,24 @@ public class SleepAlarmWidget extends AppWidgetProvider {
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if (ACTION.equals(intent.getAction())) {
- int userSleepDuration = new ActivityUser().getActivityUserSleepDuration();
+ int userSleepDuration = new ActivityUser().getSleepDuration();
// current timestamp
GregorianCalendar calendar = new GregorianCalendar();
// add preferred sleep duration
calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration);
- int hours = calendar.get(calendar.HOUR_OF_DAY);
- int minutes = calendar.get(calendar.MINUTE);
// overwrite the first alarm and activate it
- GBAlarm alarm = new GBAlarm(0, true, true, Alarm.ALARM_ONCE, hours, minutes);
+ GBAlarm alarm = GBAlarm.createSingleShot(0, true, calendar);
alarm.store();
if (GBApplication.isRunningLollipopOrLater()) {
setAlarmViaAlarmManager(context, calendar.getTimeInMillis());
}
+ int hours = calendar.get(Calendar.HOUR_OF_DAY);
+ int minutes = calendar.get(Calendar.MINUTE);
+
GB.toast(context,
String.format(context.getString(R.string.appwidget_alarms_set), hours, minutes),
Toast.LENGTH_SHORT, GB.INFO);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java
index 965a0cfd6..8d9ebc103 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java
@@ -1,23 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
-import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
-import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.Nullable;
import android.support.v4.app.NavUtils;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatDelegate;
-import android.support.v7.widget.Toolbar;
import android.text.InputType;
-import android.view.MenuInflater;
import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,10 +21,9 @@ import nodomain.freeyourgadget.gadgetbridge.R;
* to set that listener in #onCreate, *not* in #onPostCreate, otherwise the value will
* not be displayed.
*/
-public abstract class AbstractSettingsActivity extends PreferenceActivity {
+public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivity {
private static final Logger LOG = LoggerFactory.getLogger(AbstractSettingsActivity.class);
- private AppCompatDelegate delegate;
/**
* A preference value change listener that updates the preference's summary
@@ -44,7 +33,7 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity {
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
if (preference instanceof EditTextPreference) {
- if (((EditTextPreference) preference).getEditText().getKeyListener().getInputType() == InputType.TYPE_CLASS_NUMBER) {
+ if ((((EditTextPreference) preference).getEditText().getKeyListener().getInputType() & InputType.TYPE_CLASS_NUMBER) != 0) {
if ("".equals(String.valueOf(value))) {
// reject empty numeric input
return false;
@@ -104,15 +93,12 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity {
} else {
setTheme(R.style.GadgetbridgeTheme);
}
- getDelegate().installViewFactory();
- getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
- getDelegate().onPostCreate(savedInstanceState);
for (String prefKey : getPreferenceKeysWithSummary()) {
final Preference pref = findPreference(prefKey);
@@ -124,67 +110,6 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity {
}
}
-
- @Override
- protected void onPostResume() {
- super.onPostResume();
- getDelegate().onPostResume();
- }
-
- @Override
- protected void onTitleChanged(CharSequence title, int color) {
- super.onTitleChanged(title, color);
- getDelegate().setTitle(title);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- getDelegate().onConfigurationChanged(newConfig);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- getDelegate().onStop();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- getDelegate().onDestroy();
- }
-
- @Override
- public MenuInflater getMenuInflater() {
- return getDelegate().getMenuInflater();
- }
-
- @Override
- public void setContentView(@LayoutRes int layoutResID) {
- getDelegate().setContentView(layoutResID);
- }
-
- @Override
- public void setContentView(View view) {
- getDelegate().setContentView(view);
- }
-
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().setContentView(view, params);
- }
-
- @Override
- public void addContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().addContentView(view, params);
- }
-
- public void invalidateOptionsMenu() {
- getDelegate().invalidateOptionsMenu();
- }
-
-
/**
* Subclasses should reimplement this to return the keys of those
* preferences which should print its values as a summary below the
@@ -236,19 +161,4 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity {
}
return super.onOptionsItemSelected(item);
}
-
- public ActionBar getSupportActionBar() {
- return getDelegate().getSupportActionBar();
- }
-
- public void setSupportActionBar(@Nullable Toolbar toolbar) {
- getDelegate().setSupportActionBar(toolbar);
- }
-
- private AppCompatDelegate getDelegate() {
- if (delegate == null) {
- delegate = AppCompatDelegate.create(this, null);
- }
- return delegate;
- }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppCompatPreferenceActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppCompatPreferenceActivity.java
new file mode 100644
index 000000000..40f21e345
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppCompatPreferenceActivity.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2014 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 nodomain.freeyourgadget.gadgetbridge.activities;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ *
+ * This technique can be used with an {@link android.app.Activity} class, not just
+ * {@link android.preference.PreferenceActivity}.
+ */
+public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
+
+ private AppCompatDelegate mDelegate;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getDelegate().installViewFactory();
+ getDelegate().onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ getDelegate().onPostCreate(savedInstanceState);
+ }
+
+ public ActionBar getSupportActionBar() {
+ return getDelegate().getSupportActionBar();
+ }
+
+ public void setSupportActionBar(@Nullable Toolbar toolbar) {
+ getDelegate().setSupportActionBar(toolbar);
+ }
+
+ @Override
+ public MenuInflater getMenuInflater() {
+ return getDelegate().getMenuInflater();
+ }
+
+ @Override
+ public void setContentView(@LayoutRes int layoutResID) {
+ getDelegate().setContentView(layoutResID);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ getDelegate().setContentView(view);
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().setContentView(view, params);
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().addContentView(view, params);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ getDelegate().onPostResume();
+ }
+
+ @Override
+ protected void onTitleChanged(CharSequence title, int color) {
+ super.onTitleChanged(title, color);
+ getDelegate().setTitle(title);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ getDelegate().onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getDelegate().onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ getDelegate().onDestroy();
+ }
+
+ @Override
+ public void invalidateOptionsMenu() {
+ getDelegate().invalidateOptionsMenu();
+ }
+
+ private AppCompatDelegate getDelegate() {
+ if (mDelegate == null) {
+ mDelegate = AppCompatDelegate.create(this, null);
+ }
+ return mDelegate;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java
deleted file mode 100644
index f66bd7310..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java
+++ /dev/null
@@ -1,279 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.activities;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.v4.app.NavUtils;
-import android.support.v4.content.LocalBroadcastManager;
-import android.view.ContextMenu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ListView;
-
-import org.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.UUID;
-
-import nodomain.freeyourgadget.gadgetbridge.GBApplication;
-import nodomain.freeyourgadget.gadgetbridge.R;
-import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
-import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
-import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
-import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
-import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
-import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
-
-
-public class AppManagerActivity extends GBActivity {
- public static final String ACTION_REFRESH_APPLIST
- = "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
- private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class);
-
- private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (action.equals(GBApplication.ACTION_QUIT)) {
- finish();
- } else if (action.equals(ACTION_REFRESH_APPLIST)) {
- int appCount = intent.getIntExtra("app_count", 0);
- for (Integer i = 0; i < appCount; i++) {
- String appName = intent.getStringExtra("app_name" + i.toString());
- String appCreator = intent.getStringExtra("app_creator" + i.toString());
- UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
- GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
-
- boolean found = false;
- for (final ListIterator iter = appList.listIterator(); iter.hasNext(); ) {
- final GBDeviceApp app = iter.next();
- if (app.getUUID().equals(uuid)) {
- app.setOnDevice(true);
- iter.set(app);
- found = true;
- break;
- }
- }
- if (!found) {
- GBDeviceApp app = new GBDeviceApp(uuid, appName, appCreator, "", appType);
- app.setOnDevice(true);
- appList.add(app);
- }
- }
-
- mGBDeviceAppAdapter.notifyDataSetChanged();
- }
- }
- };
-
- private Prefs prefs;
-
- private final List appList = new ArrayList<>();
- private GBDeviceAppAdapter mGBDeviceAppAdapter;
- private GBDeviceApp selectedApp = null;
- private GBDevice mGBDevice = null;
-
- private List getSystemApps() {
- List systemApps = new ArrayList<>();
- if (prefs.getBoolean("pebble_force_untested", false)) {
- systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
- systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
- }
- if (mGBDevice != null && !"aplite".equals(PebbleUtils.getPlatformName(mGBDevice.getHardwareVersion()))) {
- systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
- }
-
- return systemApps;
- }
-
- private List getCachedApps() {
- List cachedAppList = new ArrayList<>();
- File cachePath;
- try {
- cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
- } catch (IOException e) {
- LOG.warn("could not get external dir while reading pbw cache.");
- return cachedAppList;
- }
-
- File files[] = cachePath.listFiles();
- if (files != null) {
- for (File file : files) {
- if (file.getName().endsWith(".pbw")) {
- String baseName = file.getName().substring(0, file.getName().length() - 4);
- //metadata
- File jsonFile = new File(cachePath, baseName + ".json");
- //configuration
- File configFile = new File(cachePath, baseName + "_config.js");
- try {
- String jsonstring = FileUtils.getStringFromFile(jsonFile);
- JSONObject json = new JSONObject(jsonstring);
- cachedAppList.add(new GBDeviceApp(json, configFile.exists()));
- } catch (Exception e) {
- LOG.warn("could not read json file for " + baseName, e.getMessage(), e);
- cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN));
- }
- }
- }
- }
- return cachedAppList;
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Bundle extras = getIntent().getExtras();
- if (extras != null) {
- mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
- } else {
- throw new IllegalArgumentException("Must provide a device when invoking this activity");
- }
-
- prefs = GBApplication.getPrefs();
-
- setContentView(R.layout.activity_appmanager);
-
- ListView appListView = (ListView) findViewById(R.id.appListView);
- mGBDeviceAppAdapter = new GBDeviceAppAdapter(this, appList);
- appListView.setAdapter(this.mGBDeviceAppAdapter);
-
- appListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView parent, View v, int position, long id) {
- UUID uuid = appList.get(position).getUUID();
- GBApplication.deviceService().onAppStart(uuid, true);
- }
- });
-
- registerForContextMenu(appListView);
-
- appList.addAll(getCachedApps());
-
- appList.addAll(getSystemApps());
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(GBApplication.ACTION_QUIT);
- filter.addAction(ACTION_REFRESH_APPLIST);
-
- LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
-
- GBApplication.deviceService().onAppInfoReq();
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- super.onCreateContextMenu(menu, v, menuInfo);
- getMenuInflater().inflate(R.menu.appmanager_context, menu);
- AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
- selectedApp = appList.get(acmi.position);
-
- if (!selectedApp.isInCache()) {
- menu.removeItem(R.id.appmanager_app_reinstall);
- menu.removeItem(R.id.appmanager_app_delete_cache);
- }
- if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) {
- menu.removeItem(R.id.appmanager_health_activate);
- menu.removeItem(R.id.appmanager_health_deactivate);
- }
- if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM) {
- menu.removeItem(R.id.appmanager_app_delete);
- }
- if (!selectedApp.isConfigurable()) {
- menu.removeItem(R.id.appmanager_app_configure);
- }
- menu.setHeaderTitle(selectedApp.getName());
- }
-
- private void removeAppFromList(UUID uuid) {
- for (final ListIterator iter = appList.listIterator(); iter.hasNext(); ) {
- final GBDeviceApp app = iter.next();
- if (app.getUUID().equals(uuid)) {
- iter.remove();
- mGBDeviceAppAdapter.notifyDataSetChanged();
- break;
- }
- }
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.appmanager_health_deactivate:
- case R.id.appmanager_app_delete_cache:
- String baseName;
- try {
- baseName = FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID();
- } catch (IOException e) {
- LOG.warn("could not get external dir while trying to access pbw cache.");
- return true;
- }
-
- String[] suffixToDelete = new String[]{".pbw", ".json", "_config.js"};
-
- for (String suffix : suffixToDelete) {
- File fileToDelete = new File(baseName + suffix);
- if (!fileToDelete.delete()) {
- LOG.warn("could not delete file from pbw cache: " + fileToDelete.toString());
- } else {
- LOG.info("deleted file: " + fileToDelete.toString());
- }
- }
- removeAppFromList(selectedApp.getUUID());
- // fall through
- case R.id.appmanager_app_delete:
- GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
- return true;
- case R.id.appmanager_app_reinstall:
- File cachePath;
- try {
- cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID() + ".pbw");
- } catch (IOException e) {
- LOG.warn("could not get external dir while trying to access pbw cache.");
- return true;
- }
- GBApplication.deviceService().onInstallApp(Uri.fromFile(cachePath));
- return true;
- case R.id.appmanager_health_activate:
- GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
- return true;
- case R.id.appmanager_app_configure:
- GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);
-
- Intent startIntent = new Intent(getApplicationContext(), ExternalPebbleJSActivity.class);
- startIntent.putExtra("app_uuid", selectedApp.getUUID());
- startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
- startActivity(startIntent);
- return true;
- default:
- return super.onContextItemSelected(item);
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- NavUtils.navigateUpFromSameTask(this);
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- protected void onDestroy() {
- LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
- super.onDestroy();
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java
index 37de8193f..6b9178fb7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java
@@ -3,8 +3,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
+import android.app.AlertDialog;
import android.app.ProgressDialog;
-import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
@@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
import de.cketti.library.changelog.ChangeLog;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@@ -41,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -50,18 +50,17 @@ public class ControlCenter extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ControlCenter.class);
- public static final String ACTION_REFRESH_DEVICELIST
- = "nodomain.freeyourgadget.gadgetbridge.controlcenter.action.set_version";
-
private TextView hintTextView;
private FloatingActionButton fab;
private ImageView background;
private SwipeRefreshLayout swipeLayout;
private GBDeviceAdapter mGBDeviceAdapter;
- private GBDevice selectedDevice = null;
-
- private final List deviceList = new ArrayList<>();
+ private DeviceManager deviceManager;
+ /**
+ * Temporary field for the context menu
+ */
+ private GBDevice selectedDevice;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
@@ -71,48 +70,20 @@ public class ControlCenter extends GBActivity {
case GBApplication.ACTION_QUIT:
finish();
break;
- case ACTION_REFRESH_DEVICELIST:
- case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
+ case DeviceManager.ACTION_DEVICES_CHANGED:
refreshPairedDevices();
- break;
- case GBDevice.ACTION_DEVICE_CHANGED:
- GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
- if (dev.getAddress() != null) {
- int index = deviceList.indexOf(dev); // search by address
- if (index >= 0) {
- deviceList.set(index, dev);
- } else {
- deviceList.add(dev);
- }
+ GBDevice selectedDevice = deviceManager.getSelectedDevice();
+ if (selectedDevice != null) {
+ refreshBusyState(selectedDevice);
+ enableSwipeRefresh(selectedDevice);
}
- updateSelectedDevice(dev);
- refreshPairedDevices();
-
- refreshBusyState(dev);
- enableSwipeRefresh(selectedDevice);
break;
}
}
};
- private void updateSelectedDevice(GBDevice dev) {
- if (selectedDevice == null) {
- selectedDevice = dev;
- } else {
- if (!selectedDevice.equals(dev)) {
- if (selectedDevice.isConnected() && dev.isConnected()) {
- LOG.warn("multiple connected devices -- this is currently not really supported");
- selectedDevice = dev; // use the last one that changed
- }
- if (!selectedDevice.isConnected()) {
- selectedDevice = dev; // use the last one that changed
- }
- }
- }
- }
-
private void refreshBusyState(GBDevice dev) {
- if (dev.isBusy()) {
+ if (dev != null && dev.isBusy()) {
swipeLayout.setRefreshing(true);
} else {
boolean wasBusy = swipeLayout.isRefreshing();
@@ -120,7 +91,6 @@ public class ControlCenter extends GBActivity {
swipeLayout.setRefreshing(false);
}
}
- mGBDeviceAdapter.notifyDataSetChanged();
}
@Override
@@ -128,6 +98,8 @@ public class ControlCenter extends GBActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_controlcenter);
+ deviceManager = ((GBApplication)getApplication()).getDeviceManager();
+
hintTextView = (TextView) findViewById(R.id.hintTextView);
ListView deviceListView = (ListView) findViewById(R.id.deviceListView);
fab = (FloatingActionButton) findViewById(R.id.fab);
@@ -140,12 +112,13 @@ public class ControlCenter extends GBActivity {
}
});
+ final List deviceList = deviceManager.getDevices();
mGBDeviceAdapter = new GBDeviceAdapter(this, deviceList);
deviceListView.setAdapter(this.mGBDeviceAdapter);
deviceListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View v, int position, long id) {
- GBDevice gbDevice = deviceList.get(position);
+ GBDevice gbDevice = mGBDeviceAdapter.getItem(position);
if (gbDevice.isInitialized()) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
Class extends Activity> primaryActivity = coordinator.getPrimaryActivity();
@@ -155,7 +128,7 @@ public class ControlCenter extends GBActivity {
startActivity(startIntent);
}
} else {
- GBApplication.deviceService().connect(deviceList.get(position));
+ GBApplication.deviceService().connect(gbDevice);
}
}
});
@@ -172,13 +145,9 @@ public class ControlCenter extends GBActivity {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
- filterLocal.addAction(ACTION_REFRESH_DEVICELIST);
- filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
- filterLocal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
- registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
-
refreshPairedDevices();
/*
* Ask for permission to intercept notifications on first run.
@@ -200,7 +169,7 @@ public class ControlCenter extends GBActivity {
GBApplication.deviceService().start();
- enableSwipeRefresh(selectedDevice);
+ enableSwipeRefresh(deviceManager.getSelectedDevice());
if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
startActivity(new Intent(this, DiscoveryActivity.class));
} else {
@@ -212,7 +181,7 @@ public class ControlCenter extends GBActivity {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
- selectedDevice = deviceList.get(acmi.position);
+ selectedDevice = mGBDeviceAdapter.getItem(acmi.position);
if (selectedDevice != null && selectedDevice.isBusy()) {
// no context menu when device is busy
return;
@@ -229,6 +198,9 @@ public class ControlCenter extends GBActivity {
if (!coordinator.supportsAlarmConfiguration()) {
menu.removeItem(R.id.controlcenter_configure_alarms);
}
+ if (!coordinator.supportsActivityTracking()) {
+ menu.removeItem(R.id.controlcenter_start_sleepmonitor);
+ }
if (selectedDevice.getState() == GBDevice.State.NOT_CONNECTED) {
menu.removeItem(R.id.controlcenter_disconnect);
@@ -254,6 +226,7 @@ public class ControlCenter extends GBActivity {
}
private void fetchActivityData() {
+ GBDevice selectedDevice = deviceManager.getSelectedDevice();
if (selectedDevice == null) {
return;
}
@@ -313,6 +286,11 @@ public class ControlCenter extends GBActivity {
GBApplication.deviceService().onScreenshotReq();
}
return true;
+ case R.id.controlcenter_delete_device:
+ if (selectedDevice != null) {
+ confirmDeleteDevice(selectedDevice);
+ }
+ return true;
default:
return super.onContextItemSelected(item);
}
@@ -341,11 +319,12 @@ public class ControlCenter extends GBActivity {
Intent debugIntent = new Intent(this, DebugActivity.class);
startActivity(debugIntent);
return true;
+ case R.id.action_db_management:
+ Intent dbIntent = new Intent(this, DbManagementActivity.class);
+ startActivity(dbIntent);
+ return true;
case R.id.action_quit:
- GBApplication.deviceService().quit();
-
- Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
- LocalBroadcastManager.getInstance(this).sendBroadcast(quitIntent);
+ GBApplication.quit();
return true;
}
@@ -356,25 +335,51 @@ public class ControlCenter extends GBActivity {
startActivity(new Intent(this, DiscoveryActivity.class));
}
+ private void confirmDeleteDevice(final GBDevice gbDevice) {
+ new AlertDialog.Builder(this)
+ .setCancelable(true)
+ .setTitle(getString(R.string.controlcenter_delete_device_name, gbDevice.getName()))
+ .setMessage(R.string.controlcenter_delete_device_dialogmessage)
+ .setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+ if (coordinator != null) {
+ coordinator.deleteDevice(selectedDevice);
+ }
+ DeviceHelper.getInstance().removeBond(selectedDevice);
+ } catch (Exception ex) {
+ GB.toast(ControlCenter.this, "Error deleting device: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
+ } finally {
+ selectedDevice = null;
+ Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
+ LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
+ }
+ }
+ })
+ .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // do nothing
+ }
+ })
+ .show();
+ }
+
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
- unregisterReceiver(mReceiver);
super.onDestroy();
}
private void refreshPairedDevices() {
- Set availableDevices = DeviceHelper.getInstance().getAvailableDevices(this);
- deviceList.retainAll(availableDevices);
- for (GBDevice availableDevice : availableDevices) {
- if (!deviceList.contains(availableDevice)) {
- deviceList.add(availableDevice);
- }
- }
- boolean connected = false;
+ List deviceList = deviceManager.getDevices();
+ GBDevice connectedDevice = null;
+
for (GBDevice device : deviceList) {
if (device.isConnected() || device.isConnecting()) {
- connected = true;
+ connectedDevice = device;
break;
}
}
@@ -385,8 +390,8 @@ public class ControlCenter extends GBActivity {
background.setVisibility(View.INVISIBLE);
}
- if (connected) {
- DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(selectedDevice);
+ if (connectedDevice != null) {
+ DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(connectedDevice);
hintTextView.setText(coordinator.getTapString());
} else if (!deviceList.isEmpty()) {
hintTextView.setText(R.string.tap_a_device_to_connect);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java
new file mode 100644
index 000000000..07e0d22a5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java
@@ -0,0 +1,278 @@
+package nodomain.freeyourgadget.gadgetbridge.activities;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.IntentFilter;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.NavUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
+import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+
+public class DbManagementActivity extends GBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(DbManagementActivity.class);
+
+ private Button exportDBButton;
+ private Button importDBButton;
+ private Button importOldActivityDataButton;
+ private Button deleteOldActivityDBButton;
+ private Button deleteDBButton;
+ private TextView dbPath;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_db_management);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(GBApplication.ACTION_QUIT);
+
+ dbPath = (TextView) findViewById(R.id.activity_db_management_path);
+ dbPath.setText(getExternalPath());
+
+ exportDBButton = (Button) findViewById(R.id.exportDBButton);
+ exportDBButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ exportDB();
+ }
+ });
+ importDBButton = (Button) findViewById(R.id.importDBButton);
+ importDBButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ importDB();
+ }
+ });
+
+ boolean hasOldDB = hasOldActivityDatabase();
+ int oldDBVisibility = hasOldDB ? View.VISIBLE : View.GONE;
+
+ View oldDBTitle = findViewById(R.id.mergeOldActivityDataTitle);
+ oldDBTitle.setVisibility(oldDBVisibility);
+ View oldDBText = findViewById(R.id.mergeOldActivityDataText);
+ oldDBText.setVisibility(oldDBVisibility);
+
+ importOldActivityDataButton = (Button) findViewById(R.id.mergeOldActivityData);
+ importOldActivityDataButton.setVisibility(oldDBVisibility);
+ importOldActivityDataButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mergeOldActivityDbContents();
+ }
+ });
+
+ deleteOldActivityDBButton = (Button) findViewById(R.id.deleteOldActivityDB);
+ deleteOldActivityDBButton.setVisibility(oldDBVisibility);
+ deleteOldActivityDBButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ deleteOldActivityDbFile();
+ }
+ });
+
+ deleteDBButton = (Button) findViewById(R.id.emptyDBButton);
+ deleteDBButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ deleteActivityDatabase();
+ }
+ });
+ }
+
+ private boolean hasOldActivityDatabase() {
+ return new DBHelper(this).getOldActivityDatabaseHandler() != null;
+ }
+
+ private String getExternalPath() {
+ try {
+ return FileUtils.getExternalFilesDir().getAbsolutePath();
+ } catch (Exception ex) {
+ LOG.warn("Unable to get external files dir", ex);
+ }
+ return getString(R.string.dbmanagementactivvity_cannot_access_export_path);
+ }
+
+ private void exportDB() {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DBHelper helper = new DBHelper(this);
+ File dir = FileUtils.getExternalFilesDir();
+ File destFile = helper.exportDB(dbHandler, dir);
+ GB.toast(this, getString(R.string.dbmanagementactivity_exported_to, destFile.getAbsolutePath()), Toast.LENGTH_LONG, GB.INFO);
+ } catch (Exception ex) {
+ GB.toast(this, getString(R.string.dbmanagementactivity_error_exporting_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
+ }
+ }
+
+ private void importDB() {
+ new AlertDialog.Builder(this)
+ .setCancelable(true)
+ .setTitle(R.string.dbmanagementactivity_import_data_title)
+ .setMessage(R.string.dbmanagementactivity_overwrite_database_confirmation)
+ .setPositiveButton(R.string.dbmanagementactivity_overwrite, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DBHelper helper = new DBHelper(DbManagementActivity.this);
+ File dir = FileUtils.getExternalFilesDir();
+ SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
+ File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
+ helper.importDB(dbHandler, sourceFile);
+ helper.validateDB(sqLiteOpenHelper);
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_import_successful), Toast.LENGTH_LONG, GB.INFO);
+ } catch (Exception ex) {
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
+ }
+ }
+ })
+ .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ })
+ .show();
+ }
+
+ private void mergeOldActivityDbContents() {
+ final DBHelper helper = new DBHelper(getBaseContext());
+ final ActivityDatabaseHandler oldHandler = helper.getOldActivityDatabaseHandler();
+ if (oldHandler == null) {
+ GB.toast(this, getString(R.string.dbmanagementactivity_no_old_activitydatabase_found), Toast.LENGTH_LONG, GB.ERROR);
+ return;
+ }
+ selectDeviceForMergingActivityDatabaseInto(new DeviceSelectionCallback() {
+ @Override
+ public void invoke(final GBDevice device) {
+ if (device == null) {
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_no_connected_device), Toast.LENGTH_LONG, GB.ERROR);
+ return;
+ }
+ try (DBHandler targetHandler = GBApplication.acquireDB()) {
+ final ProgressDialog progress = ProgressDialog.show(DbManagementActivity.this, getString(R.string.dbmanagementactivity_merging_activity_data_title), getString(R.string.dbmanagementactivity_please_wait_while_merging), true, false);
+ new AsyncTask() {
+ @Override
+ protected Object doInBackground(Object[] params) {
+ helper.importOldDb(oldHandler, device, targetHandler);
+ if (!isFinishing() && !isDestroyed()) {
+ progress.dismiss();
+ }
+ return null;
+ }
+ }.execute((Object[]) null);
+ } catch (Exception ex) {
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_old_activity_data), Toast.LENGTH_LONG, GB.ERROR, ex);
+ }
+ }
+ });
+ }
+
+ private void selectDeviceForMergingActivityDatabaseInto(final DeviceSelectionCallback callback) {
+ GBDevice connectedDevice = ((GBApplication)getApplication()).getDeviceManager().getSelectedDevice();
+ if (connectedDevice == null) {
+ callback.invoke(null);
+ return;
+ }
+ final List availableDevices = Collections.singletonList(connectedDevice);
+ GBDeviceAdapter adapter = new GBDeviceAdapter(getBaseContext(), availableDevices);
+
+ new AlertDialog.Builder(this)
+ .setCancelable(true)
+ .setTitle(R.string.dbmanagementactivity_associate_old_data_with_device)
+ .setAdapter(adapter, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ GBDevice device = availableDevices.get(which);
+ callback.invoke(device);
+ }
+ })
+ .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // ignore, just return
+ }
+ })
+ .show();
+ }
+
+ private void deleteActivityDatabase() {
+ new AlertDialog.Builder(this)
+ .setCancelable(true)
+ .setTitle(R.string.dbmanagementactivity_delete_activity_data_title)
+ .setMessage(R.string.dbmanagementactivity_really_delete_entire_db)
+ .setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (GBApplication.deleteActivityDatabase(DbManagementActivity.this)) {
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_database_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
+ } else {
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
+ }
+ }
+ })
+ .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ })
+ .show();
+ }
+
+ private void deleteOldActivityDbFile() {
+ new AlertDialog.Builder(this).setCancelable(true);
+ new AlertDialog.Builder(this).setTitle(R.string.dbmanagementactivity_delete_old_activity_db);
+ new AlertDialog.Builder(this).setMessage(R.string.dbmanagementactivity_delete_old_activitydb_confirmation);
+ new AlertDialog.Builder(this).setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (GBApplication.deleteOldActivityDatabase(DbManagementActivity.this)) {
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
+ } else {
+ GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
+ }
+ }
+ });
+ new AlertDialog.Builder(this).setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ });
+ new AlertDialog.Builder(this).show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ public interface DeviceSelectionCallback {
+ void invoke(GBDevice device);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java
index 3922505ef..30d0e6318 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java
@@ -1,14 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
-import android.app.AlertDialog;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
-import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.app.NotificationCompat;
@@ -16,33 +13,27 @@ import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.view.MenuItem;
import android.view.View;
+import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
+import android.widget.Spinner;
import android.widget.Toast;
-import net.e175.klaus.solarpositioning.DeltaT;
-import net.e175.klaus.solarpositioning.SPA;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
-import java.util.GregorianCalendar;
+import java.util.ArrayList;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
-import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
-import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
-import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
-import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
-import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class DebugActivity extends GBActivity {
@@ -52,8 +43,8 @@ public class DebugActivity extends GBActivity {
private static final String ACTION_REPLY
= "nodomain.freeyourgadget.gadgetbridge.DebugActivity.action.reply";
- private Button sendSMSButton;
- private Button sendEmailButton;
+ private Spinner sendTypeSpinner;
+ private Button sendButton;
private Button incomingCallButton;
private Button outgoingCallButton;
private Button startCallButton;
@@ -63,9 +54,7 @@ public class DebugActivity extends GBActivity {
private Button setTimeButton;
private Button rebootButton;
private Button HeartRateButton;
- private Button exportDBButton;
- private Button importDBButton;
- private Button deleteDBButton;
+ private Button testNewFunctionalityButton;
private EditText editContent;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@@ -105,27 +94,26 @@ public class DebugActivity extends GBActivity {
registerReceiver(mReceiver, filter); // for ACTION_REPLY
editContent = (EditText) findViewById(R.id.editContent);
- sendSMSButton = (Button) findViewById(R.id.sendSMSButton);
- sendSMSButton.setOnClickListener(new View.OnClickListener() {
+
+ ArrayList spinnerArray = new ArrayList<>();
+ for (NotificationType notificationType : NotificationType.values()) {
+ spinnerArray.add(notificationType.name());
+ }
+ ArrayAdapter spinnerArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, spinnerArray);
+ sendTypeSpinner = (Spinner) findViewById(R.id.sendTypeSpinner);
+ sendTypeSpinner.setAdapter(spinnerArrayAdapter);
+
+ sendButton = (Button) findViewById(R.id.sendButton);
+ sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NotificationSpec notificationSpec = new NotificationSpec();
- notificationSpec.phoneNumber = editContent.getText().toString();
- notificationSpec.body = editContent.getText().toString();
- notificationSpec.type = NotificationType.SMS;
- notificationSpec.id = -1;
- GBApplication.deviceService().onNotification(notificationSpec);
- }
- });
- sendEmailButton = (Button) findViewById(R.id.sendEmailButton);
- sendEmailButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- NotificationSpec notificationSpec = new NotificationSpec();
- notificationSpec.sender = getResources().getText(R.string.app_name).toString();
- notificationSpec.subject = editContent.getText().toString();
- notificationSpec.body = editContent.getText().toString();
- notificationSpec.type = NotificationType.EMAIL;
+ String testString = editContent.getText().toString();
+ notificationSpec.phoneNumber = testString;
+ notificationSpec.body = testString;
+ notificationSpec.sender = testString;
+ notificationSpec.subject = testString;
+ notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()];
notificationSpec.id = -1;
GBApplication.deviceService().onNotification(notificationSpec);
}
@@ -171,29 +159,6 @@ public class DebugActivity extends GBActivity {
}
});
- exportDBButton = (Button) findViewById(R.id.exportDBButton);
- exportDBButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- exportDB();
- }
- });
- importDBButton = (Button) findViewById(R.id.importDBButton);
- importDBButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- importDB();
- }
- });
-
- deleteDBButton = (Button) findViewById(R.id.emptyDBButton);
- deleteDBButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- deleteActivityDatabase();
- }
- });
-
rebootButton = (Button) findViewById(R.id.rebootButton);
rebootButton.setOnClickListener(new View.OnClickListener() {
@Override
@@ -250,81 +215,18 @@ public class DebugActivity extends GBActivity {
testNotification();
}
});
- }
- private void exportDB() {
- DBHandler dbHandler = null;
- try {
- dbHandler = GBApplication.acquireDB();
- DBHelper helper = new DBHelper(this);
- File dir = FileUtils.getExternalFilesDir();
- File destFile = helper.exportDB(dbHandler.getHelper(), dir);
- GB.toast(this, "Exported to: " + destFile.getAbsolutePath(), Toast.LENGTH_LONG, GB.INFO);
- } catch (Exception ex) {
- GB.toast(this, "Error exporting DB: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
- } finally {
- if (dbHandler != null) {
- dbHandler.release();
+ testNewFunctionalityButton = (Button) findViewById(R.id.testNewFunctionality);
+ testNewFunctionalityButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ testNewFunctionality();
}
- }
+ });
}
- private void importDB() {
- new AlertDialog.Builder(this)
- .setCancelable(true)
- .setTitle("Import Activity Data?")
- .setMessage("Really overwrite the current activity database? All your activity data (if any) will be lost.")
- .setPositiveButton("Overwrite", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- DBHandler dbHandler = null;
- try {
- dbHandler = GBApplication.acquireDB();
- DBHelper helper = new DBHelper(DebugActivity.this);
- File dir = FileUtils.getExternalFilesDir();
- SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
- File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
- helper.importDB(sqLiteOpenHelper, sourceFile);
- helper.validateDB(sqLiteOpenHelper);
- GB.toast(DebugActivity.this, "Import successful.", Toast.LENGTH_LONG, GB.INFO);
- } catch (Exception ex) {
- GB.toast(DebugActivity.this, "Error importing DB: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
- } finally {
- if (dbHandler != null) {
- dbHandler.release();
- }
- }
- }
- })
- .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- }
- })
- .show();
- }
-
- private void deleteActivityDatabase() {
- new AlertDialog.Builder(this)
- .setCancelable(true)
- .setTitle("Delete Activity Data?")
- .setMessage("Really delete the entire activity database? All your activity data will be lost.")
- .setPositiveButton("Delete", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (GBApplication.deleteActivityDatabase()) {
- GB.toast(DebugActivity.this, "Activity database successfully deleted.", Toast.LENGTH_SHORT, GB.INFO);
- } else {
- GB.toast(DebugActivity.this, "Activity database deletion failed.", Toast.LENGTH_SHORT, GB.INFO);
- }
- }
- })
- .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- }
- })
- .show();
+ private void testNewFunctionality() {
+ GBApplication.deviceService().onTestNewFunction();
}
private void testNotification() {
@@ -379,4 +281,7 @@ public class DebugActivity extends GBActivity {
unregisterReceiver(mReceiver);
}
+ public interface DeviceSelectionCallback {
+ void invoke(GBDevice device);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java
index 3f00ced47..b883fc1cf 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java
@@ -1,18 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
+import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
+import android.os.ParcelUuid;
import android.os.Parcelable;
import android.support.v4.app.ActivityCompat;
import android.view.View;
@@ -26,19 +34,28 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.List;
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import static android.bluetooth.le.ScanSettings.MATCH_MODE_STICKY;
+import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_LATENCY;
+
public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemClickListener {
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivity.class);
private static final long SCAN_DURATION = 60000; // 60s
+ private ScanCallback newLeScanCallback = null;
+
private final Handler handler = new Handler();
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@@ -46,16 +63,27 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
- discoveryStarted(Scanning.SCANNING_BT);
+ if (isScanning != Scanning.SCANNING_BTLE && isScanning != Scanning.SCANNING_NEW_BTLE) {
+ discoveryStarted(Scanning.SCANNING_BT);
+ }
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
- // continue with LE scan, if available
- if (isScanning == Scanning.SCANNING_BT) {
- checkAndRequestLocationPermission();
- startDiscovery(Scanning.SCANNING_BTLE);
- } else {
- discoveryFinished();
- }
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ // continue with LE scan, if available
+ if (isScanning == Scanning.SCANNING_BT) {
+ checkAndRequestLocationPermission();
+ if (GBApplication.isRunningLollipopOrLater()) {
+ startDiscovery(Scanning.SCANNING_NEW_BTLE);
+ } else {
+ startDiscovery(Scanning.SCANNING_BTLE);
+ }
+ } else {
+ discoveryFinished();
+ }
+ }
+ });
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
int oldState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);
@@ -68,6 +96,14 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
handleDeviceFound(device, rssi);
break;
}
+ case BluetoothDevice.ACTION_UUID: {
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
+ Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
+ ParcelUuid[] uuids2 = AndroidUtils.toParcelUUids(uuids);
+ handleDeviceFound(device, rssi, uuids2);
+ break;
+ }
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null && device.getAddress().equals(bondingAddress)) {
@@ -85,10 +121,54 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
+ LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1));
+ logMessageContent(scanRecord);
handleDeviceFound(device, (short) rssi);
}
};
+
+ // why use a method to get callback?
+ // because this callback need API >= 21
+ // we cant add @TARGETAPI("Lollipop") at class header
+ // so use a method with SDK check to return this callback
+ private ScanCallback getScanCallback() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ newLeScanCallback = new ScanCallback() {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ super.onScanResult(callbackType, result);
+ try {
+ ScanRecord scanRecord = result.getScanRecord();
+ ParcelUuid[] uuids = null;
+ if (scanRecord != null) {
+ //logMessageContent(scanRecord.getBytes());
+ List serviceUuids = scanRecord.getServiceUuids();
+ if (serviceUuids != null) {
+ uuids = serviceUuids.toArray(new ParcelUuid[0]);
+ }
+ }
+ LOG.warn(result.getDevice().getName() + ": " +
+ ((scanRecord != null) ? scanRecord.getBytes().length : -1));
+ handleDeviceFound(result.getDevice(), (short) result.getRssi(), uuids);
+ } catch (NullPointerException e) {
+ LOG.warn("Error handling scan result", e);
+ }
+ }
+ };
+ }
+ return newLeScanCallback;
+ }
+
+ public void logMessageContent(byte[] value) {
+ if (value != null) {
+ for (byte b : value) {
+ LOG.warn("DATA: " + String.format("0x%2x", b) + " - " + (char) (b & 0xff));
+ }
+ }
+ }
+
private final Runnable stopRunnable = new Runnable() {
@Override
public void run() {
@@ -107,6 +187,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
private enum Scanning {
SCANNING_BT,
SCANNING_BTLE,
+ SCANNING_NEW_BTLE,
SCANNING_OFF
}
@@ -135,6 +216,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
IntentFilter bluetoothIntents = new IntentFilter();
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
+ bluetoothIntents.addAction(BluetoothDevice.ACTION_UUID);
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
@@ -174,13 +256,43 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
@Override
protected void onDestroy() {
- unregisterReceiver(bluetoothReceiver);
+ try {
+ unregisterReceiver(bluetoothReceiver);
+ } catch (IllegalArgumentException e) {
+ LOG.warn("Tried to unregister Bluetooth Receiver that wasn't registered.");
+ }
super.onDestroy();
}
private void handleDeviceFound(BluetoothDevice device, short rssi) {
- GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi);
- if (DeviceHelper.getInstance().isSupported(candidate)) {
+ ParcelUuid[] uuids = device.getUuids();
+ if (uuids == null) {
+ if (device.fetchUuidsWithSdp()) {
+ return;
+ }
+ }
+
+ handleDeviceFound(device, rssi, uuids);
+ }
+
+
+ private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
+ LOG.debug("found device: " + device.getName() + ", " + device.getAddress());
+ if (LOG.isDebugEnabled()) {
+ if (uuids != null && uuids.length > 0) {
+ for (ParcelUuid uuid : uuids) {
+ LOG.debug(" supports uuid: " + uuid.toString());
+ }
+ }
+ }
+ if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
+ return; // ignore already bonded devices
+ }
+
+ GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
+ DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
+ if (deviceType.isSupported()) {
+ candidate.setDeviceType(deviceType);
int index = deviceCandidates.indexOf(candidate);
if (index >= 0) {
deviceCandidates.set(index, candidate); // replace
@@ -215,6 +327,12 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
} else {
discoveryFinished();
}
+ } else if (what == Scanning.SCANNING_NEW_BTLE) {
+ if (GB.supportsBluetoothLE()) {
+ startNEWBTLEDiscovery();
+ } else {
+ discoveryFinished();
+ }
}
} else {
discoveryFinished();
@@ -238,6 +356,8 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
stopBTDiscovery();
} else if (wasScanning == Scanning.SCANNING_BTLE) {
stopBTLEDiscovery();
+ } else if (wasScanning == Scanning.SCANNING_NEW_BTLE) {
+ stopNewBTLEDiscovery();
}
handler.removeMessages(0, stopRunnable);
}
@@ -251,6 +371,11 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
adapter.cancelDiscovery();
}
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void stopNewBTLEDiscovery() {
+ adapter.getBluetoothLeScanner().stopScan(newLeScanCallback);
+ }
+
private void bluetoothStateChanged(int oldState, int newState) {
discoveryFinished();
if (newState == BluetoothAdapter.STATE_ON) {
@@ -310,6 +435,41 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
return true;
}
+ // New BTLE Discovery use startScan (List filters,
+ // ScanSettings settings,
+ // ScanCallback callback)
+ // It's added on API21
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void startNEWBTLEDiscovery() {
+ // Only use new API when user uses Lollipop+ device
+ LOG.info("Start New BTLE Discovery");
+ handler.removeMessages(0, stopRunnable);
+ handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
+ adapter.getBluetoothLeScanner().startScan(getScanFilters(), getScanSettings(), getScanCallback());
+ }
+
+ private List getScanFilters() {
+ List allFilters = new ArrayList<>();
+ for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
+ allFilters.addAll(coordinator.createBLEScanFilters());
+ }
+ return allFilters;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private ScanSettings getScanSettings() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return new ScanSettings.Builder()
+ .setScanMode(SCAN_MODE_LOW_LATENCY)
+ .setMatchMode(MATCH_MODE_STICKY)
+ .build();
+ } else {
+ return new ScanSettings.Builder()
+ .setScanMode(SCAN_MODE_LOW_LATENCY)
+ .build();
+ }
+ }
+
private void startBTLEDiscovery() {
LOG.info("Starting BTLE Discovery");
handler.removeMessages(0, stopRunnable);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
index a5181dccc..856fe0ad1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
@@ -1,5 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
+import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -19,8 +20,14 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.BufferedWriter;
import java.io.File;
+import java.io.FileWriter;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Scanner;
import java.util.UUID;
@@ -28,6 +35,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
@@ -37,7 +45,9 @@ public class ExternalPebbleJSActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
private UUID appUuid;
+ private Uri confUri;
private GBDevice mGBDevice = null;
+ private WebView myWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -46,23 +56,15 @@ public class ExternalPebbleJSActivity extends GBActivity {
Bundle extras = getIntent().getExtras();
if (extras != null) {
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
+ appUuid = (UUID) extras.getSerializable(DeviceService.EXTRA_APP_UUID);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
- String queryString = "";
- Uri uri = getIntent().getData();
- if (uri != null) {
- //getting back with configuration data
- appUuid = UUID.fromString(uri.getHost());
- queryString = uri.getEncodedQuery();
- } else {
- appUuid = (UUID) getIntent().getSerializableExtra("app_uuid");
- }
setContentView(R.layout.activity_external_pebble_js);
- WebView myWebView = (WebView) findViewById(R.id.configureWebview);
+ myWebView = (WebView) findViewById(R.id.configureWebview);
myWebView.clearCache(true);
myWebView.setWebViewClient(new GBWebClient());
myWebView.setWebChromeClient(new GBChromeClient());
@@ -70,11 +72,39 @@ public class ExternalPebbleJSActivity extends GBActivity {
webSettings.setJavaScriptEnabled(true);
//needed to access the DOM
webSettings.setDomStorageEnabled(true);
+ //needed for localstorage
+ webSettings.setDatabaseEnabled(true);
- JSInterface gbJSInterface = new JSInterface();
+ JSInterface gbJSInterface = new JSInterface(this);
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
- myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString);
+ myWebView.loadUrl("file:///android_asset/app_config/configure.html");
+
+ }
+
+ @Override
+ protected void onNewIntent(Intent incoming) {
+ incoming.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ super.onNewIntent(incoming);
+ confUri = incoming.getData();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ String queryString = "";
+
+ if (confUri != null) {
+ //getting back with configuration data
+ try {
+ appUuid = UUID.fromString(confUri.getHost());
+ queryString = confUri.getEncodedQuery();
+ } catch (IllegalArgumentException e) {
+ GB.toast("returned uri: " + confUri.toString(), Toast.LENGTH_LONG, GB.ERROR);
+ }
+ myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString);
+ }
+
}
private JSONObject getAppConfigurationKeys() {
@@ -108,8 +138,8 @@ public class ExternalPebbleJSActivity extends GBActivity {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("http://") || url.startsWith("https://")) {
- Intent i = new Intent(Intent.ACTION_VIEW,
- Uri.parse(url));
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else {
url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json=");
@@ -123,7 +153,10 @@ public class ExternalPebbleJSActivity extends GBActivity {
private class JSInterface {
- public JSInterface() {
+ Context mContext;
+
+ public JSInterface(Context c) {
+ mContext = c;
}
@JavascriptInterface
@@ -133,23 +166,22 @@ public class ExternalPebbleJSActivity extends GBActivity {
@JavascriptInterface
public void sendAppMessage(String msg) {
- LOG.debug("from WEBVIEW: ", msg);
+ LOG.debug("from WEBVIEW: " + msg);
JSONObject knownKeys = getAppConfigurationKeys();
try {
JSONObject in = new JSONObject(msg);
JSONObject out = new JSONObject();
String inKey, outKey;
- boolean passKey = false;
+ boolean passKey;
for (Iterator key = in.keys(); key.hasNext(); ) {
passKey = false;
inKey = key.next();
outKey = null;
- int pebbleAppIndex = knownKeys.optInt(inKey);
- if (pebbleAppIndex != 0) {
+ int pebbleAppIndex = knownKeys.optInt(inKey, -1);
+ if (pebbleAppIndex != -1) {
passKey = true;
outKey = String.valueOf(pebbleAppIndex);
-
} else {
//do not discard integer keys (see https://developer.pebble.com/guides/communication/using-pebblekit-js/ )
Scanner scanner = new Scanner(inKey);
@@ -159,7 +191,7 @@ public class ExternalPebbleJSActivity extends GBActivity {
}
}
- if (passKey && outKey != null) {
+ if (passKey) {
Object obj = in.get(inKey);
if (obj instanceof Boolean) {
obj = ((Boolean) obj) ? "true" : "false";
@@ -183,8 +215,8 @@ public class ExternalPebbleJSActivity extends GBActivity {
JSONObject wi = new JSONObject();
try {
wi.put("firmware", mGBDevice.getFirmwareVersion());
- wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getHardwareVersion()));
- wi.put("model", PebbleUtils.getModel(mGBDevice.getHardwareVersion()));
+ wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getModel()));
+ wi.put("model", PebbleUtils.getModel(mGBDevice.getModel()));
//TODO: use real info
wi.put("language", "en");
} catch (JSONException e) {
@@ -208,16 +240,72 @@ public class ExternalPebbleJSActivity extends GBActivity {
return null;
}
+ @JavascriptInterface
+ public String getAppStoredPreset() {
+ try {
+ File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
+ File configurationFile = new File(destDir, appUuid.toString() + "_preset.json");
+ if (configurationFile.exists()) {
+ return FileUtils.getStringFromFile(configurationFile);
+ }
+ } catch (IOException e) {
+ GB.toast("Error reading presets", Toast.LENGTH_LONG, GB.ERROR);
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @JavascriptInterface
+ public void saveAppStoredPreset(String msg) {
+ Writer writer;
+
+ try {
+ File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
+ File presetsFile = new File(destDir, appUuid.toString() + "_preset.json");
+ writer = new BufferedWriter(new FileWriter(presetsFile));
+ writer.write(msg);
+ writer.close();
+ GB.toast("Presets stored", Toast.LENGTH_SHORT, GB.INFO);
+ } catch (IOException e) {
+ GB.toast("Error storing presets", Toast.LENGTH_LONG, GB.ERROR);
+ e.printStackTrace();
+ }
+ }
+
@JavascriptInterface
public String getAppUUID() {
return appUuid.toString();
}
+ @JavascriptInterface
+ public String getAppLocalstoragePrefix() {
+ String prefix = mGBDevice.getAddress() + appUuid.toString();
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ byte[] bytes = prefix.getBytes("UTF-8");
+ digest.update(bytes, 0, bytes.length);
+ bytes = digest.digest();
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < bytes.length; i++) {
+ sb.append(String.format("%02X", bytes[i]));
+ }
+ return sb.toString().toLowerCase();
+ } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ return prefix;
+ }
+ }
+
@JavascriptInterface
public String getWatchToken() {
- //specification says: A string that is is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
+ //specification says: A string that is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
return "gb" + appUuid.toString();
}
+
+ @JavascriptInterface
+ public void closeActivity() {
+ NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext);
+ }
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/OnboardingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/OnboardingActivity.java
new file mode 100644
index 000000000..043c3741c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/OnboardingActivity.java
@@ -0,0 +1,78 @@
+package nodomain.freeyourgadget.gadgetbridge.activities;
+
+import android.app.ProgressDialog;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class OnboardingActivity extends GBActivity {
+
+ private Button importOldActivityDataButton;
+ private TextView importOldActivityDataText;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_onboarding);
+
+ Bundle extras = getIntent().getExtras();
+
+ GBDevice device;
+ if (extras != null) {
+ device = extras.getParcelable(GBDevice.EXTRA_DEVICE);
+ } else {
+ throw new IllegalArgumentException("Must provide a device when invoking this activity");
+ }
+
+ importOldActivityDataText = (TextView) findViewById(R.id.textview_import_old_activitydata);
+ importOldActivityDataText.setText(String.format(getString(R.string.import_old_db_information), device.getName()));
+ importOldActivityDataButton = (Button) findViewById(R.id.button_import_old_activitydata);
+ final GBDevice finalDevice = device;
+ importOldActivityDataButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mergeOldActivityDbContents(finalDevice);
+ }
+ });
+ }
+
+ private void mergeOldActivityDbContents(final GBDevice device) {
+ if (device == null) {
+ return;
+ }
+
+ final DBHelper helper = new DBHelper(getBaseContext());
+ final ActivityDatabaseHandler oldHandler = helper.getOldActivityDatabaseHandler();
+ if (oldHandler == null) {
+ GB.toast(this, "No old activity database found, nothing to import.", Toast.LENGTH_LONG, GB.ERROR);
+ return;
+ }
+
+ try (DBHandler targetHandler = GBApplication.acquireDB()) {
+ final ProgressDialog progress = ProgressDialog.show(OnboardingActivity.this, "Merging Activity Data", "Please wait while merging your activity data...", true, false);
+ new AsyncTask() {
+ @Override
+ protected Object doInBackground(Object[] params) {
+ helper.importOldDb(oldHandler, device, targetHandler);
+ progress.dismiss();
+ finish();
+ return null;
+ }
+ }.execute((Object[]) null);
+ } catch (Exception ex) {
+ GB.toast(OnboardingActivity.this, "Error importing old activity data into new database.", Toast.LENGTH_LONG, GB.ERROR, ex);
+ }
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
index 41d6bc556..990c90851 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
@@ -7,6 +7,7 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.location.Criteria;
import android.location.Location;
+import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.preference.EditTextPreference;
@@ -21,14 +22,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_SLEEP_DURATION;
@@ -78,7 +83,7 @@ public class SettingsActivity extends AbstractSettingsActivity {
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
- Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
+ Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
@@ -90,7 +95,7 @@ public class SettingsActivity extends AbstractSettingsActivity {
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
- Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
+ Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
@@ -138,15 +143,32 @@ public class SettingsActivity extends AbstractSettingsActivity {
String provider = locationManager.getBestProvider(criteria, false);
if (provider != null) {
Location location = locationManager.getLastKnownLocation(provider);
- String latitude = String.format(Locale.US, "%.6g", location.getLatitude());
- String longitude = String.format(Locale.US, "%.6g", location.getLongitude());
- LOG.info("got location. Lat: " + latitude + " Lng: " + longitude);
- EditTextPreference pref_latitude = (EditTextPreference) findPreference("location_latitude");
- EditTextPreference pref_longitude = (EditTextPreference) findPreference("location_longitude");
- pref_latitude.setText(latitude);
- pref_longitude.setText(longitude);
- pref_latitude.setSummary(latitude);
- pref_longitude.setSummary(longitude);
+ if (location != null) {
+ setLocationPreferences(location);
+ } else {
+ locationManager.requestSingleUpdate(provider, new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ setLocationPreferences(location);
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ LOG.info("provider status changed to " + status + " (" + provider + ")");
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ LOG.info("provider enabled (" + provider + ")");
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ LOG.info("provider disabled (" + provider + ")");
+ GB.toast(SettingsActivity.this, getString(R.string.toast_enable_networklocationprovider), 3000, 0);
+ }
+ }, null);
+ }
} else {
LOG.warn("No location provider found, did you deny location permission?");
}
@@ -154,6 +176,25 @@ public class SettingsActivity extends AbstractSettingsActivity {
}
});
+ pref = findPreference("canned_messages_dismisscall_send");
+ pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ Prefs prefs = GBApplication.getPrefs();
+ ArrayList messages = new ArrayList<>();
+ for (int i = 1; i <= 16; i++) {
+ String message = prefs.getString("canned_message_dismisscall_" + i, null);
+ if (message != null && !message.equals("")) {
+ messages.add(message);
+ }
+ }
+ CannedMessagesSpec cannedMessagesSpec = new CannedMessagesSpec();
+ cannedMessagesSpec.type = CannedMessagesSpec.TYPE_MISSEDCALLS;
+ cannedMessagesSpec.cannedMessages = messages.toArray(new String[messages.size()]);
+ GBApplication.deviceService().onSetCannedMessages(cannedMessagesSpec);
+ return true;
+ }
+ });
+
// Get all receivers of Media Buttons
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
@@ -205,6 +246,22 @@ public class SettingsActivity extends AbstractSettingsActivity {
"canned_reply_14",
"canned_reply_15",
"canned_reply_16",
+ "canned_message_dismisscall_1",
+ "canned_message_dismisscall_2",
+ "canned_message_dismisscall_3",
+ "canned_message_dismisscall_4",
+ "canned_message_dismisscall_5",
+ "canned_message_dismisscall_6",
+ "canned_message_dismisscall_7",
+ "canned_message_dismisscall_8",
+ "canned_message_dismisscall_9",
+ "canned_message_dismisscall_10",
+ "canned_message_dismisscall_11",
+ "canned_message_dismisscall_12",
+ "canned_message_dismisscall_13",
+ "canned_message_dismisscall_14",
+ "canned_message_dismisscall_15",
+ "canned_message_dismisscall_16",
PREF_USER_YEAR_OF_BIRTH,
PREF_USER_HEIGHT_CM,
PREF_USER_WEIGHT_KG,
@@ -212,4 +269,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
};
}
+ private void setLocationPreferences(Location location) {
+ String latitude = String.format(Locale.US, "%.6g", location.getLatitude());
+ String longitude = String.format(Locale.US, "%.6g", location.getLongitude());
+ LOG.info("got location. Lat: " + latitude + " Lng: " + longitude);
+ GB.toast(SettingsActivity.this, getString(R.string.toast_aqurired_networklocation), 2000, 0);
+ EditTextPreference pref_latitude = (EditTextPreference) findPreference("location_latitude");
+ EditTextPreference pref_longitude = (EditTextPreference) findPreference("location_longitude");
+ pref_latitude.setText(latitude);
+ pref_longitude.setText(longitude);
+ pref_latitude.setSummary(latitude);
+ pref_longitude.setSummary(longitude);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/VibrationActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/VibrationActivity.java
new file mode 100644
index 000000000..888fabae0
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/VibrationActivity.java
@@ -0,0 +1,71 @@
+package nodomain.freeyourgadget.gadgetbridge.activities;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.widget.SeekBar;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+
+
+public class VibrationActivity extends GBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(VibrationActivity.class);
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case GBApplication.ACTION_QUIT: {
+ finish();
+ break;
+ }
+ }
+ }
+ };
+ private SeekBar seekBar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_vibration);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(GBApplication.ACTION_QUIT);
+ LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
+ registerReceiver(mReceiver, filter);
+
+ seekBar = (SeekBar) findViewById(R.id.vibration_seekbar);
+ seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (progress > 0) { // 1-16
+ progress = progress * 16 - 1; // max 255
+ }
+ GBApplication.deviceService().onSetConstantVibration(progress);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
+ unregisterReceiver(mReceiver);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java
new file mode 100644
index 000000000..3c0de5ae0
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java
@@ -0,0 +1,398 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.PopupMenu;
+
+import com.woxthebox.draglistview.DragListView;
+
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
+import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
+
+
+public abstract class AbstractAppManagerFragment extends Fragment {
+ public static final String ACTION_REFRESH_APPLIST
+ = "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
+
+ protected abstract List getSystemAppsInCategory();
+
+ protected abstract String getSortFilename();
+
+ protected abstract boolean isCacheManager();
+
+ protected abstract boolean filterApp(GBDeviceApp gbDeviceApp);
+
+ protected void onChangedAppOrder() {
+ List uuidList = new ArrayList<>();
+ for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getItemList()) {
+ uuidList.add(gbDeviceApp.getUUID());
+ }
+ AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuidList);
+ }
+
+ protected void refreshList() {
+ appList.clear();
+ ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename());
+ List systemApps = getSystemAppsInCategory();
+ boolean needsRewrite = false;
+ for (GBDeviceApp systemApp : systemApps) {
+ if (!uuids.contains(systemApp.getUUID())) {
+ uuids.add(systemApp.getUUID());
+ needsRewrite = true;
+ }
+ }
+ if (needsRewrite) {
+ AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuids);
+ }
+ appList.addAll(getCachedApps(uuids));
+ }
+
+ private void refreshListFromPebble(Intent intent) {
+ appList.clear();
+ int appCount = intent.getIntExtra("app_count", 0);
+ for (Integer i = 0; i < appCount; i++) {
+ String appName = intent.getStringExtra("app_name" + i.toString());
+ String appCreator = intent.getStringExtra("app_creator" + i.toString());
+ UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
+ GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
+
+ GBDeviceApp app = new GBDeviceApp(uuid, appName, appCreator, "", appType);
+ app.setOnDevice(true);
+ if (filterApp(app)) {
+ appList.add(app);
+ }
+ }
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(ACTION_REFRESH_APPLIST)) {
+ if (intent.hasExtra("app_count")) {
+ LOG.info("got app info from pebble");
+ if (!isCacheManager()) {
+ LOG.info("will refresh list based on data from pebble");
+ refreshListFromPebble(intent);
+ }
+ } else if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3 || isCacheManager()) {
+ refreshList();
+ }
+ mGBDeviceAppAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ private DragListView appListView;
+ protected final List appList = new ArrayList<>();
+ private GBDeviceAppAdapter mGBDeviceAppAdapter;
+ protected GBDevice mGBDevice = null;
+
+ protected List getCachedApps(List uuids) {
+ List cachedAppList = new ArrayList<>();
+ File cachePath;
+ try {
+ cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
+ } catch (IOException e) {
+ LOG.warn("could not get external dir while reading pbw cache.");
+ return cachedAppList;
+ }
+
+ File[] files;
+ if (uuids == null) {
+ files = cachePath.listFiles();
+ } else {
+ files = new File[uuids.size()];
+ int index = 0;
+ for (UUID uuid : uuids) {
+ files[index++] = new File(uuid.toString() + ".pbw");
+ }
+ }
+ if (files != null) {
+ for (File file : files) {
+ if (file.getName().endsWith(".pbw")) {
+ String baseName = file.getName().substring(0, file.getName().length() - 4);
+ //metadata
+ File jsonFile = new File(cachePath, baseName + ".json");
+ //configuration
+ File configFile = new File(cachePath, baseName + "_config.js");
+ try {
+ String jsonstring = FileUtils.getStringFromFile(jsonFile);
+ JSONObject json = new JSONObject(jsonstring);
+ cachedAppList.add(new GBDeviceApp(json, configFile.exists()));
+ } catch (Exception e) {
+ LOG.info("could not read json file for " + baseName);
+ //FIXME: this is really ugly, if we do not find system uuids in pbw cache add them manually. Also duplicated code
+ switch (baseName) {
+ case "8f3c8686-31a1-4f5f-91f5-01600c9bdc59":
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
+ break;
+ case "1f03293d-47af-4f28-b960-f2b02a6dd757":
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ break;
+ case "b2cae818-10f8-46df-ad2b-98ad2254a3c1":
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ break;
+ case "67a32d95-ef69-46d4-a0b9-854cc62f97f9":
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ break;
+ case "18e443ce-38fd-47c8-84d5-6d0c775fbe55":
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ break;
+ case "0863fc6a-66c5-4f62-ab8a-82ed00a98b5d":
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ break;
+ }
+ /*
+ else if (baseName.equals("4dab81a6-d2fc-458a-992c-7a1f3b96a970")) {
+ cachedAppList.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ } else if (baseName.equals("cf1e816a-9db0-4511-bbb8-f60c48ca8fac")) {
+ cachedAppList.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ }
+ */
+ if (mGBDevice != null) {
+ if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
+ if (baseName.equals(PebbleProtocol.UUID_PEBBLE_HEALTH.toString())) {
+ cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ continue;
+ }
+ }
+ if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
+ if (baseName.equals(PebbleProtocol.UUID_WORKOUT.toString())) {
+ cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ continue;
+ }
+ }
+ if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) {
+ if (baseName.equals("3af858c3-16cb-4561-91e7-f1ad2df8725f")) {
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
+ }
+ }
+ }
+ if (uuids == null) {
+ cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN));
+ }
+ }
+ }
+ }
+ }
+ return cachedAppList;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
+
+ if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3 && !isCacheManager()) {
+ appListView.setDragEnabled(false);
+ }
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_REFRESH_APPLIST);
+
+ LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver, filter);
+
+ if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3) {
+ GBApplication.deviceService().onAppInfoReq();
+ if (isCacheManager()) {
+ refreshList();
+ }
+ } else {
+ refreshList();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ View rootView = inflater.inflate(R.layout.activity_appmanager, container, false);
+
+ appListView = (DragListView) (rootView.findViewById(R.id.appListView));
+ appListView.setLayoutManager(new LinearLayoutManager(getActivity()));
+ mGBDeviceAppAdapter = new GBDeviceAppAdapter(appList, R.layout.item_with_details, R.id.item_image, this.getContext(), this);
+ appListView.setAdapter(mGBDeviceAppAdapter, false);
+ appListView.setCanDragHorizontally(false);
+ appListView.setDragListListener(new DragListView.DragListListener() {
+ @Override
+ public void onItemDragStarted(int position) {
+ }
+
+ @Override
+ public void onItemDragging(int itemPosition, float x, float y) {
+ }
+
+ @Override
+ public void onItemDragEnded(int fromPosition, int toPosition) {
+ onChangedAppOrder();
+ }
+ });
+ return rootView;
+ }
+
+ protected void sendOrderToDevice(String concatFilename) {
+ ArrayList uuids = new ArrayList<>();
+ for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getItemList()) {
+ uuids.add(gbDeviceApp.getUUID());
+ }
+ if (concatFilename != null) {
+ ArrayList concatUuids = AppManagerActivity.getUuidsFromFile(concatFilename);
+ uuids.addAll(concatUuids);
+ }
+ GBApplication.deviceService().onAppReorder(uuids.toArray(new UUID[uuids.size()]));
+ }
+
+ public boolean openPopupMenu(View view, int position) {
+ PopupMenu popupMenu = new PopupMenu(getContext(), view);
+ popupMenu.getMenuInflater().inflate(R.menu.appmanager_context, popupMenu.getMenu());
+ Menu menu = popupMenu.getMenu();
+ final GBDeviceApp selectedApp = appList.get(position);
+
+ if (!selectedApp.isInCache()) {
+ menu.removeItem(R.id.appmanager_app_reinstall);
+ menu.removeItem(R.id.appmanager_app_delete_cache);
+ }
+ if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) {
+ menu.removeItem(R.id.appmanager_health_activate);
+ menu.removeItem(R.id.appmanager_health_deactivate);
+ }
+ if (!PebbleProtocol.UUID_WORKOUT.equals(selectedApp.getUUID())) {
+ menu.removeItem(R.id.appmanager_hrm_activate);
+ menu.removeItem(R.id.appmanager_hrm_deactivate);
+ }
+ if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM || selectedApp.getType() == GBDeviceApp.Type.WATCHFACE_SYSTEM) {
+ menu.removeItem(R.id.appmanager_app_delete);
+ }
+ if (!selectedApp.isConfigurable()) {
+ menu.removeItem(R.id.appmanager_app_configure);
+ }
+ switch (selectedApp.getType()) {
+ case WATCHFACE:
+ case APP_GENERIC:
+ case APP_ACTIVITYTRACKER:
+ break;
+ default:
+ menu.removeItem(R.id.appmanager_app_openinstore);
+ }
+ //menu.setHeaderTitle(selectedApp.getName());
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ return onContextItemSelected(item, selectedApp);
+ }
+ }
+ );
+
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ popupMenu.show();
+ return true;
+ }
+
+ public boolean onContextItemSelected(MenuItem item, GBDeviceApp selectedApp) {
+ switch (item.getItemId()) {
+ case R.id.appmanager_app_delete_cache:
+ String baseName;
+ try {
+ baseName = FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID();
+ } catch (IOException e) {
+ LOG.warn("could not get external dir while trying to access pbw cache.");
+ return true;
+ }
+
+ String[] suffixToDelete = new String[]{".pbw", ".json", "_config.js", "_preset.json"};
+
+ for (String suffix : suffixToDelete) {
+ File fileToDelete = new File(baseName + suffix);
+ if (!fileToDelete.delete()) {
+ LOG.warn("could not delete file from pbw cache: " + fileToDelete.toString());
+ } else {
+ LOG.info("deleted file: " + fileToDelete.toString());
+ }
+ }
+ AppManagerActivity.deleteFromAppOrderFile("pbwcacheorder.txt", selectedApp.getUUID()); // FIXME: only if successful
+ // fall through
+ case R.id.appmanager_app_delete:
+ if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3) {
+ AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchapps", selectedApp.getUUID()); // FIXME: only if successful
+ AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchfaces", selectedApp.getUUID()); // FIXME: only if successful
+ Intent refreshIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST);
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(refreshIntent);
+ }
+ GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
+ return true;
+ case R.id.appmanager_app_reinstall:
+ File cachePath;
+ try {
+ cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID() + ".pbw");
+ } catch (IOException e) {
+ LOG.warn("could not get external dir while trying to access pbw cache.");
+ return true;
+ }
+ GBApplication.deviceService().onInstallApp(Uri.fromFile(cachePath));
+ return true;
+ case R.id.appmanager_health_activate:
+ GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
+ return true;
+ case R.id.appmanager_hrm_activate:
+ GBApplication.deviceService().onInstallApp(Uri.parse("fake://hrm"));
+ return true;
+ case R.id.appmanager_health_deactivate:
+ case R.id.appmanager_hrm_deactivate:
+ GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
+ return true;
+ case R.id.appmanager_app_configure:
+ GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);
+
+ Intent startIntent = new Intent(getContext().getApplicationContext(), ExternalPebbleJSActivity.class);
+ startIntent.putExtra(DeviceService.EXTRA_APP_UUID, selectedApp.getUUID());
+ startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
+ startActivity(startIntent);
+ return true;
+ case R.id.appmanager_app_openinstore:
+ String url = "https://apps.getpebble.com/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1?query=" + selectedApp.getName() + "&dev_settings=true";
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ startActivity(intent);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
+ super.onDestroy();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java
new file mode 100644
index 000000000..d464d0325
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java
@@ -0,0 +1,187 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.NavUtils;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v4.view.ViewPager;
+import android.view.MenuItem;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+
+
+public class AppManagerActivity extends AbstractGBFragmentActivity {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
+
+ private GBDevice mGBDevice = null;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ switch (action) {
+ case GBApplication.ACTION_QUIT:
+ finish();
+ break;
+ }
+ }
+ };
+
+ public GBDevice getGBDevice() {
+ return mGBDevice;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_fragmentappmanager);
+
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
+ } else {
+ throw new IllegalArgumentException("Must provide a device when invoking this activity");
+ }
+
+ IntentFilter filterLocal = new IntentFilter();
+ filterLocal.addAction(GBApplication.ACTION_QUIT);
+ filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
+ LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
+
+ // Set up the ViewPager with the sections adapter.
+ ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager);
+ if (viewPager != null) {
+ viewPager.setAdapter(getPagerAdapter());
+ }
+ }
+
+ @Override
+ protected AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager) {
+ return new SectionsPagerAdapter(fragmentManager);
+ }
+
+ public static synchronized void deleteFromAppOrderFile(String filename, UUID uuid) {
+ ArrayList uuids = getUuidsFromFile(filename);
+ uuids.remove(uuid);
+ rewriteAppOrderFile(filename, uuids);
+ }
+
+ public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
+
+ public SectionsPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ // getItem is called to instantiate the fragment for the given page.
+ switch (position) {
+ case 0:
+ return new AppManagerFragmentCache();
+ case 1:
+ return new AppManagerFragmentInstalledApps();
+ case 2:
+ return new AppManagerFragmentInstalledWatchfaces();
+ }
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return 3;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case 0:
+ return getString(R.string.appmanager_cached_watchapps_watchfaces);
+ case 1:
+ return getString(R.string.appmanager_installed_watchapps);
+ case 2:
+ return getString(R.string.appmanager_installed_watchfaces);
+ case 3:
+ }
+ return super.getPageTitle(position);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ static synchronized void rewriteAppOrderFile(String filename, List uuids) {
+ try {
+ FileWriter fileWriter = new FileWriter(FileUtils.getExternalFilesDir() + "/" + filename);
+ BufferedWriter out = new BufferedWriter(fileWriter);
+ for (UUID uuid : uuids) {
+ out.write(uuid.toString());
+ out.newLine();
+ }
+ out.close();
+ } catch (IOException e) {
+ LOG.warn("can't write app order to file!");
+ }
+ }
+
+ synchronized public static void addToAppOrderFile(String filename, UUID uuid) {
+ ArrayList uuids = getUuidsFromFile(filename);
+ if (!uuids.contains(uuid)) {
+ uuids.add(uuid);
+ rewriteAppOrderFile(filename, uuids);
+ }
+ }
+
+ static synchronized ArrayList getUuidsFromFile(String filename) {
+ ArrayList uuids = new ArrayList<>();
+ try {
+ FileReader fileReader = new FileReader(FileUtils.getExternalFilesDir() + "/" + filename);
+ BufferedReader in = new BufferedReader(fileReader);
+ String line;
+ while ((line = in.readLine()) != null) {
+ uuids.add(UUID.fromString(line));
+ }
+ } catch (IOException e) {
+ LOG.warn("could not read sort file");
+ }
+ return uuids;
+ }
+
+ @Override
+ protected void onDestroy() {
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
+ super.onDestroy();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentCache.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentCache.java
new file mode 100644
index 000000000..8423ffbe8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentCache.java
@@ -0,0 +1,33 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
+
+public class AppManagerFragmentCache extends AbstractAppManagerFragment {
+ @Override
+ public void refreshList() {
+ appList.clear();
+ appList.addAll(getCachedApps(null));
+ }
+
+ @Override
+ protected boolean isCacheManager() {
+ return true;
+ }
+
+ @Override
+ protected List getSystemAppsInCategory() {
+ return null;
+ }
+
+ @Override
+ public String getSortFilename() {
+ return "pbwcacheorder.txt";
+ }
+
+ @Override
+ protected boolean filterApp(GBDeviceApp gbDeviceApp) {
+ return true;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledApps.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledApps.java
new file mode 100644
index 000000000..6cf8bfbbc
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledApps.java
@@ -0,0 +1,56 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
+import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
+
+public class AppManagerFragmentInstalledApps extends AbstractAppManagerFragment {
+
+ @Override
+ protected List getSystemAppsInCategory() {
+ List systemApps = new ArrayList<>();
+ //systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ //systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ systemApps.add(new GBDeviceApp(UUID.fromString("1f03293d-47af-4f28-b960-f2b02a6dd757"), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ systemApps.add(new GBDeviceApp(UUID.fromString("b2cae818-10f8-46df-ad2b-98ad2254a3c1"), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ systemApps.add(new GBDeviceApp(UUID.fromString("67a32d95-ef69-46d4-a0b9-854cc62f97f9"), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ systemApps.add(new GBDeviceApp(UUID.fromString("18e443ce-38fd-47c8-84d5-6d0c775fbe55"), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+
+ if (mGBDevice != null) {
+ if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
+ systemApps.add(new GBDeviceApp(UUID.fromString("0863fc6a-66c5-4f62-ab8a-82ed00a98b5d"), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ }
+ if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
+ systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
+ }
+ }
+
+ return systemApps;
+ }
+
+ @Override
+ protected boolean isCacheManager() {
+ return false;
+ }
+
+ @Override
+ protected String getSortFilename() {
+ return mGBDevice.getAddress() + ".watchapps";
+ }
+
+ @Override
+ protected void onChangedAppOrder() {
+ super.onChangedAppOrder();
+ sendOrderToDevice(mGBDevice.getAddress() + ".watchfaces");
+ }
+
+ @Override
+ protected boolean filterApp(GBDeviceApp gbDeviceApp) {
+ return gbDeviceApp.getType() == GBDeviceApp.Type.APP_ACTIVITYTRACKER || gbDeviceApp.getType() == GBDeviceApp.Type.APP_GENERIC;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledWatchfaces.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledWatchfaces.java
new file mode 100644
index 000000000..b41fad044
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledWatchfaces.java
@@ -0,0 +1,42 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
+
+public class AppManagerFragmentInstalledWatchfaces extends AbstractAppManagerFragment {
+
+ @Override
+ protected List getSystemAppsInCategory() {
+ List systemWatchfaces = new ArrayList<>();
+ systemWatchfaces.add(new GBDeviceApp(UUID.fromString("8f3c8686-31a1-4f5f-91f5-01600c9bdc59"), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
+ systemWatchfaces.add(new GBDeviceApp(UUID.fromString("3af858c3-16cb-4561-91e7-f1ad2df8725f"), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
+ return systemWatchfaces;
+ }
+
+ @Override
+ protected boolean isCacheManager() {
+ return false;
+ }
+
+ @Override
+ protected String getSortFilename() {
+ return mGBDevice.getAddress() + ".watchfaces";
+ }
+
+ @Override
+ protected void onChangedAppOrder() {
+ super.onChangedAppOrder();
+ sendOrderToDevice(mGBDevice.getAddress() + ".watchapps");
+ }
+
+ @Override
+ protected boolean filterApp(GBDeviceApp gbDeviceApp) {
+ if (gbDeviceApp.getType() == GBDeviceApp.Type.WATCHFACE) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java
index b4c776eef..b1beeb773 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java
@@ -8,19 +8,25 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
+import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
+import android.util.TypedValue;
import android.view.View;
+import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
+import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
+import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.CombinedData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
+import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
import org.slf4j.Logger;
@@ -30,7 +36,6 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
-import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
@@ -45,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@@ -82,17 +88,18 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
};
private boolean mChartDirty = true;
- private boolean supportsHeartrateChart = true;
private AsyncTask refreshTask;
public boolean isChartDirty() {
return mChartDirty;
}
+ @Override
public abstract String getTitle();
- public boolean supportsHeartrate() {
- return supportsHeartrateChart;
+ public boolean supportsHeartrate(GBDevice device) {
+ DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
+ return coordinator != null && coordinator.supportsHeartRateMeasurement(device);
}
protected static final class ActivityConfig {
@@ -150,15 +157,20 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
protected void init() {
+ TypedValue runningColor = new TypedValue();
BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
- CHART_TEXT_COLOR = getResources().getColor(R.color.secondarytext);
- HEARTRATE_COLOR = getResources().getColor(R.color.chart_heartrate);
- HEARTRATE_FILL_COLOR = getResources().getColor(R.color.chart_heartrate_fill);
- AK_ACTIVITY_COLOR = getResources().getColor(R.color.chart_activity_light);
- AK_DEEP_SLEEP_COLOR = getResources().getColor(R.color.chart_light_sleep_light);
- AK_LIGHT_SLEEP_COLOR = getResources().getColor(R.color.chart_deep_sleep_light);
- AK_NOT_WORN_COLOR = getResources().getColor(R.color.chart_not_worn_light);
+ CHART_TEXT_COLOR = ContextCompat.getColor(getContext(), R.color.secondarytext);
+ HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
+ HEARTRATE_FILL_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_fill);
+ getContext().getTheme().resolveAttribute(R.attr.chart_activity, runningColor, true);
+ AK_ACTIVITY_COLOR = runningColor.data;
+ getContext().getTheme().resolveAttribute(R.attr.chart_light_sleep, runningColor, true);
+ AK_DEEP_SLEEP_COLOR = runningColor.data;
+ getContext().getTheme().resolveAttribute(R.attr.chart_deep_sleep, runningColor, true);
+ AK_LIGHT_SLEEP_COLOR = runningColor.data;
+ getContext().getTheme().resolveAttribute(R.attr.chart_not_worn, runningColor, true);
+ AK_NOT_WORN_COLOR = runningColor.data;
HEARTRATE_LABEL = getContext().getString(R.string.charts_legend_heartrate);
@@ -292,9 +304,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
return akActivity.color;
}
- protected SampleProvider getProvider(GBDevice device) {
+ protected SampleProvider extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
- return coordinator.getSampleProvider();
+ return coordinator.getSampleProvider(device, db.getDaoSession());
}
/**
@@ -305,40 +317,25 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
* @param tsFrom
* @param tsTo
*/
- protected List getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
- SampleProvider provider = getProvider(device);
- return db.getAllActivitySamples(tsFrom, tsTo, provider);
+ protected List extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ SampleProvider extends ActivitySample> provider = getProvider(db, device);
+ return provider.getAllActivitySamples(tsFrom, tsTo);
}
- private int getTSLast24Hours(int tsTo) {
- return (tsTo) - (24 * 60 * 60); // -24 hours
- }
-
- protected List getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
- SampleProvider provider = getProvider(device);
- return db.getActivitySamples(tsFrom, tsTo, provider);
+ protected List extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ SampleProvider extends AbstractActivitySample> provider = getProvider(db, device);
+ return provider.getActivitySamples(tsFrom, tsTo);
}
- protected List getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
- SampleProvider provider = getProvider(device);
- return db.getSleepSamples(tsFrom, tsTo, provider);
- }
-
- protected List getTestSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
- Calendar cal = Calendar.getInstance();
- cal.clear();
- cal.set(2015, Calendar.JUNE, 10, 6, 40);
- // ignore provided date ranges
- tsTo = (int) ((cal.getTimeInMillis() / 1000));
- tsFrom = tsTo - (24 * 60 * 60);
-
- SampleProvider provider = getProvider(device);
- return db.getAllActivitySamples(tsFrom, tsTo, provider);
+ protected List extends ActivitySample> getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ SampleProvider extends ActivitySample> provider = getProvider(db, device);
+ return provider.getSleepSamples(tsFrom, tsTo);
}
protected void configureChartDefaults(Chart> chart) {
- chart.setDescription("");
+ chart.getXAxis().setValueFormatter(new TimestampValueFormatter());
+ chart.getDescription().setText("");
// if enabled, the chart will always start at zero on the y-axis
chart.setNoDataText(getString(R.string.chart_no_data_synchronize));
@@ -349,11 +346,19 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// enable touch gestures
chart.setTouchEnabled(true);
+// commented out: this has weird bugs/sideeffects at least on WeekStepsCharts
+// where only the first Day-label is drawn, because AxisRenderer.computeAxisValues(float,float)
+// appears to have an overflow when calculating 'n' (number of entries)
+// chart.getXAxis().setGranularity(60*5);
+
setupLegend(chart);
}
protected void configureBarLineChartDefaults(BarLineChartBase> chart) {
configureChartDefaults(chart);
+ if (chart instanceof BarChart) {
+ ((BarChart) chart).setFitBars(true);
+ }
// enable scaling and dragging
chart.setDragEnabled(true);
@@ -397,12 +402,14 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
*/
protected abstract void renderCharts();
- protected DefaultChartsData refresh(GBDevice gbDevice, List samples) {
- Calendar cal = GregorianCalendar.getInstance();
- cal.clear();
- Date date;
- String dateStringFrom = "";
- String dateStringTo = "";
+ protected DefaultChartsData refresh(GBDevice gbDevice, List extends ActivitySample> samples) {
+// Calendar cal = GregorianCalendar.getInstance();
+// cal.clear();
+ TimestampTranslation tsTranslation = new TimestampTranslation();
+// Date date;
+// String dateStringFrom = "";
+// String dateStringTo = "";
+// ArrayList xLabels = null;
LOG.info("" + getTitle() + ": number of samples:" + samples.size());
CombinedData combinedData;
@@ -412,13 +419,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
int last_type = ActivityKind.TYPE_UNKNOWN;
- SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
- SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
-
int numEntries = samples.size();
- List xLabels = new ArrayList<>(numEntries);
List activityEntries = new ArrayList<>(numEntries);
- boolean hr = supportsHeartrate();
+ boolean hr = supportsHeartrate(gbDevice);
List heartrateEntries = hr ? new ArrayList(numEntries) : null;
List colors = new ArrayList<>(numEntries); // this is kinda inefficient...
int lastHrSampleIndex = -1;
@@ -426,17 +429,20 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
for (int i = 0; i < numEntries; i++) {
ActivitySample sample = samples.get(i);
int type = sample.getKind();
+ int ts = tsTranslation.shorten(sample.getTimestamp());
+// System.out.println(ts);
+// ts = i;
// determine start and end dates
- if (i == 0) {
- cal.setTimeInMillis(sample.getTimestamp() * 1000L); // make sure it's converted to long
- date = cal.getTime();
- dateStringFrom = dateFormat.format(date);
- } else if (i == samples.size() - 1) {
- cal.setTimeInMillis(sample.getTimestamp() * 1000L); // same here
- date = cal.getTime();
- dateStringTo = dateFormat.format(date);
- }
+// if (i == 0) {
+// cal.setTimeInMillis(ts * 1000L); // make sure it's converted to long
+// date = cal.getTime();
+// dateStringFrom = dateFormat.format(date);
+// } else if (i == samples.size() - 1) {
+// cal.setTimeInMillis(ts * 1000L); // same here
+// date = cal.getTime();
+// dateStringTo = dateFormat.format(date);
+// }
float movement = sample.getIntensity();
@@ -462,23 +468,23 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// value = ((float) movement) / movement_divisor;
colors.add(akActivity.color);
}
- activityEntries.add(createBarEntry(value, i));
- if (hr && isValidHeartRateValue(sample.getCustomValue())) {
- if (lastHrSampleIndex > -1 && i - lastHrSampleIndex > HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
+ activityEntries.add(createBarEntry(value, ts));
+ if (hr && isValidHeartRateValue(sample.getHeartRate())) {
+ if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 60*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
- heartrateEntries.add(createLineEntry(0, i - 1));
+ heartrateEntries.add(createLineEntry(0, ts - 1));
}
- heartrateEntries.add(createLineEntry(sample.getCustomValue(), i));
- lastHrSampleIndex = i;
+ heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
+ lastHrSampleIndex = ts;
}
String xLabel = "";
if (annotate) {
- cal.setTimeInMillis(sample.getTimestamp() * 1000L);
- date = cal.getTime();
- String dateString = annotationDateFormat.format(date);
- xLabel = dateString;
+// cal.setTimeInMillis((ts + tsOffset) * 1000L);
+// date = cal.getTime();
+// String dateString = annotationDateFormat.format(date);
+// xLabel = dateString;
// if (last_type != type) {
// if (isSleep(last_type) && !isSleep(type)) {
// // woken up
@@ -496,35 +502,35 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// chart.getXAxis().addLimitLine(line);
// }
// }
- last_type = type;
+// last_type = type;
}
- xLabels.add(xLabel);
}
-// chart.getXAxis().setValues(xLabels);
-
BarDataSet activitySet = createActivitySet(activityEntries, colors, "Activity");
// create a data object with the datasets
- combinedData = new CombinedData(xLabels);
+// combinedData = new CombinedData(xLabels);
+ combinedData = new CombinedData();
List list = new ArrayList<>();
list.add(activitySet);
- BarData barData = new BarData(xLabels, list);
- barData.setGroupSpace(0);
+ BarData barData = new BarData(list);
+ barData.setBarWidth(100f);
+// barData.setGroupSpace(0);
combinedData.setData(barData);
if (hr && heartrateEntries.size() > 0) {
LineDataSet heartrateSet = createHeartrateSet(heartrateEntries, "Heart Rate");
- LineData lineData = new LineData(xLabels, heartrateSet);
+ LineData lineData = new LineData(heartrateSet);
combinedData.setData(lineData);
}
// chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo));
// chart.setDescriptionPosition(?, ?);
} else {
- combinedData = new CombinedData(Collections.emptyList());
+ combinedData = new CombinedData();
}
- return new DefaultChartsData(combinedData);
+ IAxisValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
+ return new DefaultChartsData(combinedData, xValueFormatter);
}
protected boolean isValidHeartRateValue(int value) {
@@ -540,16 +546,16 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
* @param tsTo
* @return
*/
- protected abstract List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
+ protected abstract List extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
protected abstract void setupLegend(Chart chart);
- protected BarEntry createBarEntry(float value, int index) {
- return new BarEntry(value, index);
+ protected BarEntry createBarEntry(float value, int xValue) {
+ return new BarEntry(xValue, value);
}
- protected Entry createLineEntry(float value, int index) {
- return new Entry(value, index);
+ protected Entry createLineEntry(float value, int xValue) {
+ return new Entry(xValue, value);
}
protected BarDataSet createActivitySet(List values, List colors, String label) {
@@ -574,7 +580,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
LineDataSet set1 = new LineDataSet(values, label);
set1.setLineWidth(0.8f);
set1.setColor(HEARTRATE_COLOR);
- set1.setDrawCubic(true);
+// set1.setDrawCubic(true);
+ set1.setMode(LineDataSet.Mode.CUBIC_BEZIER);
set1.setCubicIntensity(0.1f);
set1.setDrawCircles(false);
// set1.setCircleRadius(2f);
@@ -688,8 +695,46 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
}
- protected List getSamples(DBHandler db, GBDevice device) {
- return getSamples(db, device, getTSStart(), getTSEnd());
+ protected List extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
+ int tsStart = getTSStart();
+ int tsEnd = getTSEnd();
+ List samples = (List) getSamples(db, device, tsStart, tsEnd);
+ ensureStartAndEndSamples(samples, tsStart, tsEnd);
+// List samples2 = new ArrayList<>();
+// int min = Math.min(samples.size(), 10);
+// int min = Math.min(samples.size(), 10);
+// for (int i = 0; i < min; i++) {
+// samples2.add(samples.get(i));
+// }
+// return samples2;
+ return samples;
+ }
+
+ protected void ensureStartAndEndSamples(List samples, int tsStart, int tsEnd) {
+ if (samples == null || samples.isEmpty()) {
+ return;
+ }
+ ActivitySample lastSample = samples.get(samples.size() - 1);
+ if (lastSample.getTimestamp() < tsEnd) {
+ samples.add(createTrailingActivitySample(lastSample, tsEnd));
+ }
+
+ ActivitySample firstSample = samples.get(0);
+ if (firstSample.getTimestamp() > tsStart) {
+ samples.add(createTrailingActivitySample(firstSample, tsStart));
+ }
+ }
+
+ private ActivitySample createTrailingActivitySample(ActivitySample referenceSample, int timestamp) {
+ TrailingActivitySample sample = new TrailingActivitySample();
+ if (referenceSample instanceof AbstractActivitySample) {
+ AbstractActivitySample reference = (AbstractActivitySample) referenceSample;
+ sample.setUserId(reference.getUserId());
+ sample.setDeviceId(reference.getDeviceId());
+ sample.setProvider(reference.getProvider());
+ }
+ sample.setTimestamp(timestamp);
+ return sample;
}
private int getTSEnd() {
@@ -704,15 +749,87 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
return (int) ((date.getTime() / 1000));
}
- public static class DefaultChartsData extends ChartsData {
- private final CombinedData combinedData;
+ public static class DefaultChartsData> extends ChartsData {
+ private final T data;
+ private IAxisValueFormatter xValueFormatter;
- public DefaultChartsData(CombinedData combinedData) {
- this.combinedData = combinedData;
+ public DefaultChartsData(T data, IAxisValueFormatter xValueFormatter) {
+ this.xValueFormatter = xValueFormatter;
+ this.data = data;
}
- public CombinedData getCombinedData() {
- return combinedData;
+ public IAxisValueFormatter getXValueFormatter() {
+ return xValueFormatter;
+ }
+
+ public T getData() {
+ return data;
+ }
+ }
+
+ protected static class SampleXLabelFormatter implements IAxisValueFormatter {
+ private final TimestampTranslation tsTranslation;
+ SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
+// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
+ Calendar cal = GregorianCalendar.getInstance();
+
+ public SampleXLabelFormatter(TimestampTranslation tsTranslation) {
+ this.tsTranslation = tsTranslation;
+
+ }
+ // TODO: this does not work. Cannot use precomputed labels
+ @Override
+ public String getFormattedValue(float value, AxisBase axis) {
+ cal.clear();
+ int ts = (int) value;
+ cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L);
+ Date date = cal.getTime();
+ String dateString = annotationDateFormat.format(date);
+ return dateString;
+ }
+ }
+
+ protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter {
+ private ArrayList xLabels;
+
+ public PreformattedXIndexLabelFormatter(ArrayList xLabels) {
+ this.xLabels = xLabels;
+
+ }
+ @Override
+ public String getFormattedValue(float value, AxisBase axis) {
+ int index = (int) value;
+ if (xLabels == null || index >= xLabels.size()) {
+ return String.valueOf(value);
+ }
+ return xLabels.get(index);
+ }
+ }
+
+ /**
+ * Awkward class that helps in translating long timestamp
+ * values to float (sic!) values. It basically rebases all
+ * timestamps to a base (the very first) timestamp value.
+ *
+ * It does this so that the large timestamp values can be used
+ * floating point values, where the mantissa is just 24 bits.
+ */
+ protected static class TimestampTranslation {
+ private int tsOffset = -1;
+
+ public int shorten(int timestamp) {
+ if (tsOffset == -1) {
+ tsOffset = timestamp;
+ return 0;
+ }
+ return timestamp - tsOffset;
+ }
+
+ public int toOriginalValue(int timestamp) {
+ if (tsOffset == -1) {
+ return timestamp;
+ }
+ return timestamp + tsOffset;
}
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java
index f656b0587..8d0719f23 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java
@@ -8,7 +8,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class ActivityAnalysis {
- public ActivityAmounts calculateActivityAmounts(List samples) {
+ public ActivityAmounts calculateActivityAmounts(List extends ActivitySample> samples) {
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
ActivityAmount notWorn = new ActivityAmount(ActivityKind.TYPE_NOT_WORN);
@@ -17,7 +17,7 @@ public class ActivityAnalysis {
ActivityAmount previousAmount = null;
ActivitySample previousSample = null;
for (ActivitySample sample : samples) {
- ActivityAmount amount = null;
+ ActivityAmount amount;
switch (sample.getKind()) {
case ActivityKind.TYPE_DEEP_SLEEP:
amount = deepSleep;
@@ -43,8 +43,6 @@ public class ActivityAnalysis {
previousAmount.addSeconds(sharedTimeDifference);
amount.addSeconds(sharedTimeDifference);
}
- } else {
- // nothing to do, we can only calculate when we have the next sample
}
previousAmount = amount;
@@ -66,10 +64,13 @@ public class ActivityAnalysis {
return result;
}
- public int calculateTotalSteps(List samples) {
+ public int calculateTotalSteps(List extends ActivitySample> samples) {
int totalSteps = 0;
for (ActivitySample sample : samples) {
- totalSteps += sample.getSteps();
+ int steps = sample.getSteps();
+ if (steps > 0) {
+ totalSteps += sample.getSteps();
+ }
}
return totalSteps;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java
index 7260a205e..43be544f9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java
@@ -10,6 +10,7 @@ import android.view.ViewGroup;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
+import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
@@ -55,7 +56,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
private void setupChart() {
mChart.setBackgroundColor(BACKGROUND_COLOR);
- mChart.setDescriptionColor(DESCRIPTION_COLOR);
+ mChart.getDescription().setTextColor(DESCRIPTION_COLOR);
configureBarLineChartDefaults(mChart);
@@ -70,8 +71,8 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
y.setDrawGridLines(false);
// y.setDrawLabels(false);
// TODO: make fixed max value optional
- y.setAxisMaxValue(1f);
- y.setAxisMinValue(0);
+ y.setAxisMaximum(1f);
+ y.setAxisMinimum(0);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
@@ -80,12 +81,12 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
YAxis yAxisRight = mChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
- yAxisRight.setEnabled(supportsHeartrate());
+ yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
yAxisRight.setDrawLabels(true);
yAxisRight.setDrawTopYLabelEntry(true);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
- yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE);
- yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE);
+ yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
+ yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
@@ -108,7 +109,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
- List samples = getSamples(db, device);
+ List extends ActivitySample> samples = getSamples(db, device);
return refresh(device, samples);
}
@@ -116,33 +117,52 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
protected void updateChartsnUIThread(ChartsData chartsData) {
DefaultChartsData dcd = (DefaultChartsData) chartsData;
mChart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
- mChart.setData(dcd.getCombinedData());
- }
-
- protected void renderCharts() {
- mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
- }
-
- protected void setupLegend(Chart chart) {
- List legendColors = new ArrayList<>(4);
- List legendLabels = new ArrayList<>(4);
- legendColors.add(akActivity.color);
- legendLabels.add(akActivity.label);
- legendColors.add(akLightSleep.color);
- legendLabels.add(akLightSleep.label);
- legendColors.add(akDeepSleep.color);
- legendLabels.add(akDeepSleep.label);
- legendColors.add(akNotWorn.color);
- legendLabels.add(akNotWorn.label);
- if (supportsHeartrate()) {
- legendColors.add(HEARTRATE_COLOR);
- legendLabels.add(HEARTRATE_LABEL);
- }
- chart.getLegend().setCustom(legendColors, legendLabels);
+ mChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
+ mChart.getXAxis().setValueFormatter(dcd.getXValueFormatter());
+ mChart.setData(dcd.getData());
}
@Override
- protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ protected void renderCharts() {
+ mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
+// mChart.invalidate();
+ }
+
+ @Override
+ protected void setupLegend(Chart chart) {
+ List legendEntries = new ArrayList<>(5);
+
+ LegendEntry activityEntry = new LegendEntry();
+ activityEntry.label = akActivity.label;
+ activityEntry.formColor = akActivity.color;
+ legendEntries.add(activityEntry);
+
+ LegendEntry lightSleepEntry = new LegendEntry();
+ lightSleepEntry.label = akLightSleep.label;
+ lightSleepEntry.formColor = akLightSleep.color;
+ legendEntries.add(lightSleepEntry);
+
+ LegendEntry deepSleepEntry = new LegendEntry();
+ deepSleepEntry.label = akDeepSleep.label;
+ deepSleepEntry.formColor = akDeepSleep.color;
+ legendEntries.add(deepSleepEntry);
+
+ LegendEntry notWornEntry = new LegendEntry();
+ notWornEntry.label = akNotWorn.label;
+ notWornEntry.formColor = akNotWorn.color;
+ legendEntries.add(notWornEntry);
+
+ if (supportsHeartrate(getChartsHost().getDevice())) {
+ LegendEntry hrEntry = new LegendEntry();
+ hrEntry.label = HEARTRATE_LABEL;
+ hrEntry.formColor = HEARTRATE_COLOR;
+ legendEntries.add(hrEntry);
+ }
+ chart.getLegend().setCustom(legendEntries);
+ }
+
+ @Override
+ protected List extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return getAllSamples(db, device, tsFrom, tsTo);
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java
index f09fca794..292202cca 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java
@@ -102,8 +102,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
swipeLayout.setRefreshing(true);
} else {
boolean wasBusy = swipeLayout.isRefreshing();
+ swipeLayout.setRefreshing(false);
if (wasBusy) {
- swipeLayout.setRefreshing(false);
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java
index 19a62a4b4..12b1d152d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java
@@ -69,12 +69,13 @@ public class LiveActivityFragment extends AbstractChartFragment {
private List heartRateValues;
private LineDataSet mHeartRateSet;
private int mHeartRate;
+ private TimestampTranslation tsTranslation;
private class Steps {
private int initialSteps;
private int steps;
- private long lastTimestamp;
+ private int lastTimestamp;
private int currentStepsPerMinute;
private int maxStepsPerMinute;
private int lastStepsPerMinute;
@@ -96,7 +97,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
return maxStepsPerMinute;
}
- public void updateCurrentSteps(int newSteps, long timestamp) {
+ public void updateCurrentSteps(int newSteps, int timestamp) {
try {
if (steps == 0) {
steps = newSteps;
@@ -110,7 +111,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
if (newSteps >= steps) {
int stepsDelta = newSteps - steps;
- long timeDelta = timestamp - lastTimestamp;
+ int timeDelta = timestamp - lastTimestamp;
currentStepsPerMinute = calculateStepsPerMinute(stepsDelta, timeDelta);
if (currentStepsPerMinute > maxStepsPerMinute) {
maxStepsPerMinute = currentStepsPerMinute;
@@ -127,16 +128,16 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
}
- private int calculateStepsPerMinute(int stepsDelta, long millis) {
+ private int calculateStepsPerMinute(int stepsDelta, int seconds) {
if (stepsDelta == 0) {
return 0; // not walking or not enough data per mills?
}
- if (millis <= 0) {
- throw new IllegalArgumentException("delta in millis is <= 0 -- time change?");
+ if (seconds <= 0) {
+ throw new IllegalArgumentException("delta in seconds is <= 0 -- time change?");
}
- long oneMinute = 60 * 1000;
- float factor = oneMinute / millis;
+ int oneMinute = 60 * 1000;
+ float factor = oneMinute / seconds;
int result = (int) (stepsDelta * factor);
if (result > MAX_STEPS_PER_MINUTE) {
// ignore, return previous value instead
@@ -153,13 +154,13 @@ public class LiveActivityFragment extends AbstractChartFragment {
switch (action) {
case DeviceService.ACTION_REALTIME_STEPS: {
int steps = intent.getIntExtra(DeviceService.EXTRA_REALTIME_STEPS, 0);
- long timestamp = intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ int timestamp = translateTimestampFrom(intent);
addEntries(steps, timestamp);
break;
}
case DeviceService.ACTION_HEARTRATE_MEASUREMENT: {
int heartRate = intent.getIntExtra(DeviceService.EXTRA_HEART_RATE_VALUE, 0);
- long timestamp = intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ int timestamp = translateTimestampFrom(intent);
if (isValidHeartRateValue(heartRate)) {
setCurrentHeartRate(heartRate, timestamp);
}
@@ -169,7 +170,16 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
};
- private void setCurrentHeartRate(int heartRate, long timestamp) {
+ private int translateTimestampFrom(Intent intent) {
+ return translateTimestamp(intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()));
+ }
+
+ private int translateTimestamp(long tsMillis) {
+ int timestamp = (int) (tsMillis / 1000); // translate to seconds
+ return tsTranslation.shorten(timestamp); // and shorten
+ }
+
+ private void setCurrentHeartRate(int heartRate, int timestamp) {
addHistoryDataSet(true);
mHeartRate = heartRate;
}
@@ -180,7 +190,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
return result;
}
- private void addEntries(int steps, long timestamp) {
+ private void addEntries(int steps, int timestamp) {
mSteps.updateCurrentSteps(steps, timestamp);
if (++maxStepsResetCounter > RESET_COUNT) {
maxStepsResetCounter = 0;
@@ -192,7 +202,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
// addEntries();
}
- private void addEntries() {
+ private void addEntries(int timestamp) {
mTotalStepsChart.setSingleEntryYValue(mSteps.getTotalSteps());
YAxis stepsPerMinuteCurrentYAxis = mStepsPerMinuteCurrentChart.getAxisLeft();
int maxStepsPerMinute = mSteps.getMaxStepsPerMinute();
@@ -211,16 +221,15 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
ChartData data = mStepsPerMinuteHistoryChart.getData();
- data.addXValue("");
if (stepsPerMinute < 0) {
stepsPerMinute = 0;
}
- mHistorySet.addEntry(new Entry(stepsPerMinute, data.getXValCount() - 1));
+ mHistorySet.addEntry(new Entry(timestamp, stepsPerMinute));
int hr = getCurrentHeartRate();
if (hr < 0) {
hr = 0;
}
- mHeartRateSet.addEntry(new Entry(hr, data.getXValCount() - 1));
+ mHeartRateSet.addEntry(new Entry(timestamp, hr));
}
private boolean addHistoryDataSet(boolean force) {
@@ -245,6 +254,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
filterLocal.addAction(DeviceService.ACTION_REALTIME_STEPS);
filterLocal.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
heartRateValues = new ArrayList<>();
+ tsTranslation = new TimestampTranslation();
View rootView = inflater.inflate(R.layout.fragment_live_activity, container, false);
@@ -252,8 +262,8 @@ public class LiveActivityFragment extends AbstractChartFragment {
mTotalStepsChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_total);
mStepsPerMinuteHistoryChart = (BarLineChartBase) rootView.findViewById(R.id.livechart_steps_per_minute_history);
- totalStepsEntry = new BarEntry(0, 1);
- stepsPerMinuteEntry = new BarEntry(0, 1);
+ totalStepsEntry = new BarEntry(1, 0);
+ stepsPerMinuteEntry = new BarEntry(1, 0);
mStepsPerMinuteData = setupCurrentChart(mStepsPerMinuteCurrentChart, stepsPerMinuteEntry, getString(R.string.live_activity_current_steps_per_minute));
mTotalStepsData = setupTotalStepsChart(mTotalStepsChart, totalStepsEntry, getString(R.string.live_activity_total_steps));
@@ -305,7 +315,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
* Called in the UI thread.
*/
private void pulse() {
- addEntries();
+ addEntries(translateTimestamp(System.currentTimeMillis()));
LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData();
if (historyData == null) {
@@ -323,7 +333,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
}
- private long getPulseIntervalMillis() {
+ private int getPulseIntervalMillis() {
return 1000;
}
@@ -368,32 +378,31 @@ public class LiveActivityFragment extends AbstractChartFragment {
chart.getXAxis().setDrawLabels(false);
chart.getXAxis().setEnabled(false);
chart.setBackgroundColor(BACKGROUND_COLOR);
- chart.setDescriptionColor(DESCRIPTION_COLOR);
- chart.setDescription(title);
- chart.setNoDataTextDescription("");
+ chart.getDescription().setTextColor(DESCRIPTION_COLOR);
+ chart.getDescription().setText(title);
+// chart.setNoDataTextDescription("");
chart.setNoDataText("");
chart.getAxisRight().setEnabled(false);
List entries = new ArrayList<>();
- List xLabels = new ArrayList<>();
List colors = new ArrayList<>();
entries.add(new BarEntry(0, 0));
entries.add(entry);
- entries.add(new BarEntry(0, 2));
+ entries.add(new BarEntry(2, 0));
colors.add(akActivity.color);
colors.add(akActivity.color);
colors.add(akActivity.color);
- //we don't want labels
- xLabels.add("");
- xLabels.add("");
- xLabels.add("");
+// //we don't want labels
+// xLabels.add("");
+// xLabels.add("");
+// xLabels.add("");
BarDataSet set = new BarDataSet(entries, "");
set.setDrawValues(false);
set.setColors(colors);
- BarData data = new BarData(xLabels, set);
- data.setGroupSpace(0);
+ BarData data = new BarData(set);
+// data.setGroupSpace(0);
chart.setData(data);
chart.getLegend().setEnabled(false);
@@ -402,7 +411,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
private BarDataSet setupTotalStepsChart(CustomBarChart chart, BarEntry entry, String label) {
- mTotalStepsChart.getAxisLeft().setAxisMaxValue(5000); // TODO: use daily goal - already reached steps
+ mTotalStepsChart.getAxisLeft().setAxisMaximum(5000); // TODO: use daily goal - already reached steps
return setupCommonChart(chart, entry, label); // at the moment, these look the same
}
@@ -411,8 +420,8 @@ public class LiveActivityFragment extends AbstractChartFragment {
chart.setTouchEnabled(false); // no zooming or anything, because it's updated all the time
chart.setBackgroundColor(BACKGROUND_COLOR);
- chart.setDescriptionColor(DESCRIPTION_COLOR);
- chart.setDescription(getString(R.string.live_activity_steps_per_minute_history));
+ chart.getDescription().setTextColor(DESCRIPTION_COLOR);
+ chart.getDescription().setText(getString(R.string.live_activity_steps_per_minute_history));
chart.setNoDataText(getString(R.string.live_activity_start_your_activity));
chart.getLegend().setEnabled(false);
Paint infoPaint = chart.getPaint(Chart.PAINT_INFO);
@@ -432,7 +441,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setEnabled(true);
- y.setAxisMinValue(0);
+ y.setAxisMinimum(0);
YAxis yAxisRight = chart.getAxisRight();
yAxisRight.setDrawGridLines(false);
@@ -440,14 +449,14 @@ public class LiveActivityFragment extends AbstractChartFragment {
yAxisRight.setDrawLabels(true);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
- yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE);
- yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE);
+ yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
+ yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
mHistorySet = new LineDataSet(new ArrayList(), getString(R.string.live_activity_steps_history));
mHistorySet.setAxisDependency(YAxis.AxisDependency.LEFT);
mHistorySet.setColor(akActivity.color);
mHistorySet.setDrawCircles(false);
- mHistorySet.setDrawCubic(true);
+ mHistorySet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
mHistorySet.setDrawFilled(true);
mHistorySet.setDrawValues(false);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java
index 3655f3bac..76476c731 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java
@@ -23,8 +23,8 @@ public class SingleEntryValueAnimator extends ChartAnimator {
}
public void setEntryYValue(float value) {
- this.previousValue = entry.getVal();
- entry.setVal(value);
+ this.previousValue = entry.getY();
+ entry.setY(value);
}
@Override
@@ -38,10 +38,10 @@ public class SingleEntryValueAnimator extends ChartAnimator {
float startAnim;
float endAnim = 1f;
- if (entry.getVal() == 0f) {
+ if (entry.getY() == 0f) {
startAnim = 0f;
} else {
- startAnim = previousValue / entry.getVal();
+ startAnim = previousValue / entry.getY();
}
// LOG.debug("anim factors: " + startAnim + ", " + endAnim);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java
index 48ff18f84..5df0d587f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java
@@ -11,12 +11,15 @@ import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.CombinedChart;
import com.github.mikephil.charting.charts.PieChart;
+import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
+import com.github.mikephil.charting.data.CombinedData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
-import com.github.mikephil.charting.formatter.ValueFormatter;
+import com.github.mikephil.charting.data.PieEntry;
+import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import org.slf4j.Logger;
@@ -50,7 +53,7 @@ public class SleepChartFragment extends AbstractChartFragment {
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
- List samples = getSamples(db, device);
+ List extends ActivitySample> samples = getSamples(db, device);
MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples);
DefaultChartsData chartsData = refresh(device, samples);
@@ -58,26 +61,27 @@ public class SleepChartFragment extends AbstractChartFragment {
return new MyChartsData(mySleepChartsData, chartsData);
}
- private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List samples) {
+ private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List extends ActivitySample> samples) {
ActivityAnalysis analysis = new ActivityAnalysis();
ActivityAmounts amounts = analysis.calculateActivityAmounts(samples);
PieData data = new PieData();
- List entries = new ArrayList<>();
+ List entries = new ArrayList<>();
List colors = new ArrayList<>();
- int index = 0;
+// int index = 0;
long totalSeconds = 0;
for (ActivityAmount amount : amounts.getAmounts()) {
if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) {
long value = amount.getTotalSeconds();
totalSeconds += value;
- entries.add(new Entry(value, index++));
+// entries.add(new PieEntry(value, index++));
+ entries.add(new PieEntry(value, amount.getName(getActivity())));
colors.add(getColorFor(amount.getActivityKind()));
- data.addXValue(amount.getName(getActivity()));
+// data.addXValue(amount.getName(getActivity()));
}
}
String totalSleep = DateTimeUtils.formatDurationHoursMinutes(totalSeconds, TimeUnit.SECONDS);
PieDataSet set = new PieDataSet(entries, "");
- set.setValueFormatter(new ValueFormatter() {
+ set.setValueFormatter(new IValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
@@ -96,7 +100,9 @@ public class SleepChartFragment extends AbstractChartFragment {
mSleepAmountChart.setCenterText(mcd.getPieData().getTotalSleep());
mSleepAmountChart.setData(mcd.getPieData().getPieData());
- mActivityChart.setData(mcd.getChartsData().getCombinedData());
+ mActivityChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
+ mActivityChart.getXAxis().setValueFormatter(mcd.getChartsData().getXValueFormatter());
+ mActivityChart.setData(mcd.getChartsData().getData());
}
@Override
@@ -138,16 +144,16 @@ public class SleepChartFragment extends AbstractChartFragment {
private void setupSleepAmountChart() {
mSleepAmountChart.setBackgroundColor(BACKGROUND_COLOR);
- mSleepAmountChart.setDescriptionColor(DESCRIPTION_COLOR);
- mSleepAmountChart.setDescription("");
- mSleepAmountChart.setNoDataTextDescription("");
+ mSleepAmountChart.getDescription().setTextColor(DESCRIPTION_COLOR);
+ mSleepAmountChart.getDescription().setText("");
+// mSleepAmountChart.getDescription().setNoDataTextDescription("");
mSleepAmountChart.setNoDataText("");
mSleepAmountChart.getLegend().setEnabled(false);
}
private void setupActivityChart() {
mActivityChart.setBackgroundColor(BACKGROUND_COLOR);
- mActivityChart.setDescriptionColor(DESCRIPTION_COLOR);
+ mActivityChart.getDescription().setTextColor(DESCRIPTION_COLOR);
configureBarLineChartDefaults(mActivityChart);
XAxis x = mActivityChart.getXAxis();
@@ -161,8 +167,8 @@ public class SleepChartFragment extends AbstractChartFragment {
y.setDrawGridLines(false);
// y.setDrawLabels(false);
// TODO: make fixed max value optional
- y.setAxisMaxValue(1f);
- y.setAxisMinValue(0);
+ y.setAxisMaximum(1f);
+ y.setAxisMinimum(0);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
@@ -171,7 +177,7 @@ public class SleepChartFragment extends AbstractChartFragment {
YAxis yAxisRight = mActivityChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
- yAxisRight.setEnabled(supportsHeartrate());
+ yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
yAxisRight.setDrawLabels(true);
yAxisRight.setDrawTopYLabelEntry(true);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
@@ -179,28 +185,37 @@ public class SleepChartFragment extends AbstractChartFragment {
yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE);
}
+ @Override
protected void setupLegend(Chart chart) {
- List legendColors = new ArrayList<>(2);
- List legendLabels = new ArrayList<>(2);
- legendColors.add(akLightSleep.color);
- legendLabels.add(akLightSleep.label);
- legendColors.add(akDeepSleep.color);
- legendLabels.add(akDeepSleep.label);
- if (supportsHeartrate()) {
- legendColors.add(HEARTRATE_COLOR);
- legendLabels.add(HEARTRATE_LABEL);
+ List legendEntries = new ArrayList<>(3);
+ LegendEntry lightSleepEntry = new LegendEntry();
+ lightSleepEntry.label = akLightSleep.label;
+ lightSleepEntry.formColor = akLightSleep.color;
+ legendEntries.add(lightSleepEntry);
+
+ LegendEntry deepSleepEntry = new LegendEntry();
+ deepSleepEntry.label = akDeepSleep.label;
+ deepSleepEntry.formColor = akDeepSleep.color;
+ legendEntries.add(deepSleepEntry);
+
+ if (supportsHeartrate(getChartsHost().getDevice())) {
+ LegendEntry hrEntry = new LegendEntry();
+ hrEntry.label = HEARTRATE_LABEL;
+ hrEntry.formColor = HEARTRATE_COLOR;
+ legendEntries.add(hrEntry);
}
- chart.getLegend().setCustom(legendColors, legendLabels);
+ chart.getLegend().setCustom(legendEntries);
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
@Override
- protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ protected List extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
// temporary fix for totally wrong sleep amounts
// return super.getSleepSamples(db, device, tsFrom, tsTo);
return super.getAllSamples(db, device, tsFrom, tsTo);
}
+ @Override
protected void renderCharts() {
mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
mSleepAmountChart.invalidate();
@@ -225,10 +240,10 @@ public class SleepChartFragment extends AbstractChartFragment {
}
private static class MyChartsData extends ChartsData {
- private final DefaultChartsData chartsData;
+ private final DefaultChartsData chartsData;
private final MySleepChartsData pieData;
- public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData) {
+ public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData) {
this.pieData = pieData;
this.chartsData = chartsData;
}
@@ -237,7 +252,7 @@ public class SleepChartFragment extends AbstractChartFragment {
return pieData;
}
- public DefaultChartsData getChartsData() {
+ public DefaultChartsData getChartsData() {
return chartsData;
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java
new file mode 100644
index 000000000..437330cb3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java
@@ -0,0 +1,35 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.charts;
+
+import com.github.mikephil.charting.components.AxisBase;
+import com.github.mikephil.charting.formatter.IAxisValueFormatter;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+
+public class TimestampValueFormatter implements IAxisValueFormatter {
+ private final Calendar cal;
+ // private DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
+ private DateFormat dateFormat;
+
+ public TimestampValueFormatter() {
+ this(new SimpleDateFormat("HH:mm"));
+
+ }
+
+ public TimestampValueFormatter(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ cal = GregorianCalendar.getInstance();
+ cal.clear();
+ }
+
+ @Override
+ public String getFormattedValue(float value, AxisBase axis) {
+ cal.setTimeInMillis((int) value * 1000L);
+ Date date = cal.getTime();
+ String dateString = dateFormat.format(date);
+ return dateString;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TrailingActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TrailingActivitySample.java
new file mode 100644
index 000000000..4c2fc77a4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TrailingActivitySample.java
@@ -0,0 +1,39 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.charts;
+
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+
+public class TrailingActivitySample extends AbstractActivitySample {
+ private int timestamp;
+ private long userId;
+ private long deviceId;
+
+ @Override
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public void setUserId(long userId) {
+ this.userId = userId;
+ }
+
+ @Override
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ @Override
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ @Override
+ public long getUserId() {
+ return userId;
+ }
+
+ @Override
+ public int getTimestamp() {
+ return timestamp;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java
index 2e271f286..3d08ed5e6 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java
@@ -6,8 +6,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.Chart;
-import com.github.mikephil.charting.charts.CombinedChart;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
@@ -15,10 +15,9 @@ import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
-import com.github.mikephil.charting.data.CombinedData;
-import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
+import com.github.mikephil.charting.data.PieEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,7 +42,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
private int mTargetSteps = 10000;
private PieChart mTodayStepsChart;
- private CombinedChart mWeekStepsChart;
+ private BarChart mWeekStepsChart;
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
@@ -64,8 +63,10 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
mTodayStepsChart.setCenterText(NumberFormat.getNumberInstance(mLocale).format(mcd.getDaySteps().totalSteps));
mTodayStepsChart.setData(mcd.getDaySteps().data);
- mWeekStepsChart.setData(mcd.getWeekBeforeStepsData().getCombinedData());
+ mWeekStepsChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
+ mWeekStepsChart.setData(mcd.getWeekBeforeStepsData().getData());
mWeekStepsChart.getLegend().setEnabled(false);
+ mWeekStepsChart.getXAxis().setValueFormatter(mcd.getWeekBeforeStepsData().getXValueFormatter());
}
@Override
@@ -74,17 +75,17 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
mTodayStepsChart.invalidate();
}
- private DefaultChartsData refreshWeekBeforeSteps(DBHandler db, CombinedChart combinedChart, Calendar day, GBDevice device) {
+ private DefaultChartsData refreshWeekBeforeSteps(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
ActivityAnalysis analysis = new ActivityAnalysis();
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -7);
List entries = new ArrayList<>();
- List labels = new ArrayList<>();
+ ArrayList labels = new ArrayList();
for (int counter = 0; counter < 7; counter++) {
- entries.add(new BarEntry(analysis.calculateTotalSteps(getSamplesOfDay(db, day, device)), counter));
+ entries.add(new BarEntry(counter, analysis.calculateTotalSteps(getSamplesOfDay(db, day, device))));
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
day.add(Calendar.DATE, 1);
}
@@ -92,39 +93,32 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
BarDataSet set = new BarDataSet(entries, "");
set.setColor(akActivity.color);
- BarData barData = new BarData(labels, set);
+ BarData barData = new BarData(set);
barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false);
LimitLine target = new LimitLine(mTargetSteps);
- combinedChart.getAxisLeft().removeAllLimitLines();
- combinedChart.getAxisLeft().addLimitLine(target);
+ barChart.getAxisLeft().removeAllLimitLines();
+ barChart.getAxisLeft().addLimitLine(target);
- CombinedData combinedData = new CombinedData(labels);
- combinedData.setData(barData);
- return new DefaultChartsData(combinedData);
+ return new DefaultChartsData(barData, new PreformattedXIndexLabelFormatter(labels));
}
-
private DaySteps refreshDaySteps(DBHandler db, Calendar day, GBDevice device) {
ActivityAnalysis analysis = new ActivityAnalysis();
int totalSteps = analysis.calculateTotalSteps(getSamplesOfDay(db, day, device));
PieData data = new PieData();
- List entries = new ArrayList<>();
+ List entries = new ArrayList<>();
List colors = new ArrayList<>();
- entries.add(new Entry(totalSteps, 0));
+ entries.add(new PieEntry(totalSteps, "")); //we don't want labels on the pie chart
colors.add(akActivity.color);
- //we don't want labels on the pie chart
- data.addXValue("");
if (totalSteps < mTargetSteps) {
- entries.add(new Entry((mTargetSteps - totalSteps), 1));
+ entries.add(new PieEntry((mTargetSteps - totalSteps))); //we don't want labels on the pie chart
colors.add(Color.GRAY);
- //we don't want labels on the pie chart
- data.addXValue("");
}
PieDataSet set = new PieDataSet(entries, "");
@@ -141,7 +135,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
- View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false);
+ View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
GBDevice device = getChartsHost().getDevice();
if (device != null) {
@@ -149,8 +143,8 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
mTargetSteps = MiBandCoordinator.getFitnessGoal(device.getAddress());
}
- mWeekStepsChart = (CombinedChart) rootView.findViewById(R.id.sleepchart);
- mTodayStepsChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep);
+ mTodayStepsChart = (PieChart) rootView.findViewById(R.id.todaystepschart);
+ mWeekStepsChart = (BarChart) rootView.findViewById(R.id.weekstepschart);
setupWeekStepsChart();
setupTodayStepsChart();
@@ -168,9 +162,9 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
private void setupTodayStepsChart() {
mTodayStepsChart.setBackgroundColor(BACKGROUND_COLOR);
- mTodayStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
- mTodayStepsChart.setDescription(getContext().getString(R.string.weeksteps_today_steps_description, mTargetSteps));
- mTodayStepsChart.setNoDataTextDescription("");
+ mTodayStepsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
+ mTodayStepsChart.getDescription().setText(getContext().getString(R.string.weeksteps_today_steps_description, String.valueOf(mTargetSteps)));
+// mTodayStepsChart.setNoDataTextDescription("");
mTodayStepsChart.setNoDataText("");
mTodayStepsChart.getLegend().setEnabled(false);
// setupLegend(mTodayStepsChart);
@@ -178,8 +172,9 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
private void setupWeekStepsChart() {
mWeekStepsChart.setBackgroundColor(BACKGROUND_COLOR);
- mWeekStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
- mWeekStepsChart.setDescription("");
+ mWeekStepsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
+ mWeekStepsChart.getDescription().setText("");
+ mWeekStepsChart.setFitBars(true);
configureBarLineChartDefaults(mWeekStepsChart);
@@ -189,11 +184,15 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
+ x.setPosition(XAxis.XAxisPosition.BOTTOM);
YAxis y = mWeekStepsChart.getAxisLeft();
y.setDrawGridLines(false);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
+ y.setDrawZeroLine(true);
+ y.setSpaceBottom(0);
+ y.setAxisMinimum(0);
y.setEnabled(true);
@@ -205,6 +204,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
yAxisRight.setTextColor(CHART_TEXT_COLOR);
}
+ @Override
protected void setupLegend(Chart chart) {
// List legendColors = new ArrayList<>(1);
// List legendLabels = new ArrayList<>(1);
@@ -214,7 +214,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
// chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
- private List getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
+ private List extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
int startTs;
int endTs;
@@ -233,7 +233,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
}
@Override
- protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ protected List extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
@@ -248,10 +248,10 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
}
private static class MyChartsData extends ChartsData {
- private final DefaultChartsData weekBeforeStepsData;
+ private final DefaultChartsData weekBeforeStepsData;
private final DaySteps daySteps;
- public MyChartsData(DaySteps daySteps, DefaultChartsData weekBeforeStepsData) {
+ public MyChartsData(DaySteps daySteps, DefaultChartsData weekBeforeStepsData) {
this.daySteps = daySteps;
this.weekBeforeStepsData = weekBeforeStepsData;
}
@@ -260,7 +260,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
return daySteps;
}
- public DefaultChartsData getWeekBeforeStepsData() {
+ public DefaultChartsData getWeekBeforeStepsData() {
return weekBeforeStepsData;
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java
index 48252eb8b..a80e3faca 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java
@@ -51,6 +51,7 @@ public class DeviceCandidateAdapter extends ArrayAdapter {
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
break;
case MIBAND:
+ case MIBAND2:
deviceImageView.setImageResource(R.drawable.ic_device_miband);
break;
default:
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java
index 92ca09b5f..3e3a25a2b 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java
@@ -120,12 +120,20 @@ public class GBDeviceAdapter extends ArrayAdapter {
}
break;
case MIBAND:
+ case MIBAND2:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_device_miband);
} else {
deviceImageView.setImageResource(R.drawable.ic_device_miband_disabled);
}
break;
+ case VIBRATISSIMO:
+ if (device.isConnected()) {
+ deviceImageView.setImageResource(R.drawable.ic_device_lovetoy);
+ } else {
+ deviceImageView.setImageResource(R.drawable.ic_device_lovetoy_disabled);
+ }
+ break;
default:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_launcher);
@@ -160,8 +168,8 @@ public class GBDeviceAdapter extends ArrayAdapter {
private String getUniqueDeviceName(GBDevice device) {
String deviceName = device.getName();
if (!isUniqueDeviceName(device, deviceName)) {
- if (device.getHardwareVersion() != null) {
- deviceName = deviceName + " " + device.getHardwareVersion();
+ if (device.getModel() != null) {
+ deviceName = deviceName + " " + device.getModel();
if (!isUniqueDeviceName(device, deviceName)) {
deviceName = deviceName + " " + device.getShortAddress();
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java
index d4ae263f3..35549ae80 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java
@@ -4,69 +4,102 @@ import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
-import java.util.List;
+import com.woxthebox.draglistview.DragItemAdapter;
+import java.util.List;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
/**
* Adapter for displaying GBDeviceApp instances.
*/
-public class GBDeviceAppAdapter extends ArrayAdapter {
- private final Context context;
+public class GBDeviceAppAdapter extends DragItemAdapter {
- public GBDeviceAppAdapter(Context context, List appList) {
- super(context, 0, appList);
+ private final int mLayoutId;
+ private final int mGrabHandleId;
+ private final Context mContext;
+ private final AbstractAppManagerFragment mParentFragment;
- this.context = context;
+ public GBDeviceAppAdapter(List list, int layoutId, int grabHandleId, Context context, AbstractAppManagerFragment parentFragment) {
+ super(true); // longpress
+ mLayoutId = layoutId;
+ mGrabHandleId = grabHandleId;
+ mContext = context;
+ mParentFragment = parentFragment;
+ setHasStableIds(true);
+ setItemList(list);
}
@Override
- public View getView(int position, View view, ViewGroup parent) {
- GBDeviceApp deviceApp = getItem(position);
+ public long getItemId(int position) {
+ return mItemList.get(position).getUUID().getLeastSignificantBits();
+ }
- if (view == null) {
- LayoutInflater inflater = (LayoutInflater) context
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- view = inflater.inflate(R.layout.item_with_details, parent, false);
- }
- TextView deviceAppVersionAuthorLabel = (TextView) view.findViewById(R.id.item_details);
- TextView deviceAppNameLabel = (TextView) view.findViewById(R.id.item_name);
- ImageView deviceImageView = (ImageView) view.findViewById(R.id.item_image);
+ View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
+ return new ViewHolder(view);
+ }
- deviceAppVersionAuthorLabel.setText(getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ super.onBindViewHolder(holder, position);
+ GBDeviceApp deviceApp = mItemList.get(position);
+
+ holder.mDeviceAppVersionAuthorLabel.setText(GBApplication.getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
// FIXME: replace with small icons
String appNameLabelText = deviceApp.getName();
- if (deviceApp.isInCache() || deviceApp.isOnDevice()) {
- appNameLabelText += " (" + (deviceApp.isInCache() ? "C" : "")
- + (deviceApp.isOnDevice() ? "D" : "") + ")";
- }
- deviceAppNameLabel.setText(appNameLabelText);
+ holder.mDeviceAppNameLabel.setText(appNameLabelText);
switch (deviceApp.getType()) {
case APP_GENERIC:
- deviceImageView.setImageResource(R.drawable.ic_watchapp);
+ holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
break;
case APP_ACTIVITYTRACKER:
- deviceImageView.setImageResource(R.drawable.ic_activitytracker);
+ holder.mDeviceImageView.setImageResource(R.drawable.ic_activitytracker);
break;
case APP_SYSTEM:
- deviceImageView.setImageResource(R.drawable.ic_systemapp);
+ holder.mDeviceImageView.setImageResource(R.drawable.ic_systemapp);
break;
case WATCHFACE:
- deviceImageView.setImageResource(R.drawable.ic_watchface);
+ holder.mDeviceImageView.setImageResource(R.drawable.ic_watchface);
break;
default:
- deviceImageView.setImageResource(R.drawable.ic_watchapp);
+ holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
+ }
+ }
+
+ public class ViewHolder extends DragItemAdapter.ViewHolder {
+ TextView mDeviceAppVersionAuthorLabel;
+ TextView mDeviceAppNameLabel;
+ ImageView mDeviceImageView;
+
+ public ViewHolder(final View itemView) {
+ super(itemView, mGrabHandleId);
+ mDeviceAppVersionAuthorLabel = (TextView) itemView.findViewById(R.id.item_details);
+ mDeviceAppNameLabel = (TextView) itemView.findViewById(R.id.item_name);
+ mDeviceImageView = (ImageView) itemView.findViewById(R.id.item_image);
}
- return view;
+ @Override
+ public void onItemClicked(View view) {
+ UUID uuid = mItemList.get(getAdapterPosition()).getUUID();
+ GBApplication.deviceService().onAppStart(uuid, true);
+ }
+
+ @Override
+ public boolean onItemLongClicked(View view) {
+ return mParentFragment.openPopupMenu(view, getAdapterPosition());
+ }
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java
index ce9e1ae46..221cf410a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java
@@ -1,43 +1,35 @@
package nodomain.freeyourgadget.gadgetbridge.database;
-import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteStatement;
import android.widget.Toast;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.io.File;
-import java.util.ArrayList;
-
-import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.schema.ActivityDBCreationScript;
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.database.schema.SchemaMigration;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.DATABASE_NAME;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_CUSTOM_SHORT;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
+/**
+ * @deprecated can be removed entirely, only used for backwards compatibility
+ */
public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandler {
- private static final Logger LOG = LoggerFactory.getLogger(ActivityDatabaseHandler.class);
-
private static final int DATABASE_VERSION = 7;
+ private static final String UPDATER_CLASS_NAME_PREFIX = "ActivityDBUpdate_";
+ private final Context context;
public ActivityDatabaseHandler(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ this.context = context;
}
@Override
@@ -52,135 +44,25 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- LOG.info("ActivityDatabase: schema upgrade requested from " + oldVersion + " to " + newVersion);
- try {
- for (int i = oldVersion + 1; i <= newVersion; i++) {
- DBUpdateScript updater = getUpdateScript(db, i);
- if (updater != null) {
- LOG.info("upgrading activity database to version " + i);
- updater.upgradeSchema(db);
- }
- }
- LOG.info("activity database is now at version " + newVersion);
- } catch (RuntimeException ex) {
- GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
- throw ex; // reject upgrade
- }
+ new SchemaMigration(UPDATER_CLASS_NAME_PREFIX).onUpgrade(db, oldVersion, newVersion);
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- LOG.info("ActivityDatabase: schema downgrade requested from " + oldVersion + " to " + newVersion);
- try {
- for (int i = oldVersion; i >= newVersion; i--) {
- DBUpdateScript updater = getUpdateScript(db, i);
- if (updater != null) {
- LOG.info("downgrading activity database to version " + (i - 1));
- updater.downgradeSchema(db);
- }
- }
- LOG.info("activity database is now at version " + newVersion);
- } catch (RuntimeException ex) {
- GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
- throw ex; // reject downgrade
- }
- }
-
- private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
- try {
- Class> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + ".schema.ActivityDBUpdate_" + version);
- return (DBUpdateScript) updateClass.newInstance();
- } catch (ClassNotFoundException e) {
- return null;
- } catch (InstantiationException | IllegalAccessException e) {
- throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
- }
- }
-
- public void addGBActivitySample(ActivitySample sample) {
- try (SQLiteDatabase db = this.getWritableDatabase()) {
- ContentValues values = new ContentValues();
- values.put(KEY_TIMESTAMP, sample.getTimestamp());
- values.put(KEY_PROVIDER, sample.getProvider().getID());
- values.put(KEY_INTENSITY, sample.getRawIntensity());
- values.put(KEY_STEPS, sample.getSteps());
- values.put(KEY_CUSTOM_SHORT, sample.getCustomValue());
- values.put(KEY_TYPE, sample.getRawKind());
-
- db.insert(TABLE_GBACTIVITYSAMPLES, null, values);
- }
- }
-
- /**
- * Adds the a new sample to the database
- *
- * @param timestamp the timestamp of the same, second-based!
- * @param provider the SampleProvider ID
- * @param intensity the sample's raw intensity value
- * @param steps the sample's steps value
- * @param kind the raw activity kind of the sample
- * @param customShortValue
- */
- @Override
- public void addGBActivitySample(int timestamp, int provider, int intensity, int steps, int kind, int customShortValue) {
- if (intensity < 0) {
- LOG.error("negative intensity received, ignoring");
- intensity = 0;
- }
- if (steps < 0) {
- LOG.error("negative steps received, ignoring");
- steps = 0;
- }
-
- if (customShortValue < 0) {
- LOG.error("negative short value received, ignoring");
- customShortValue = 0;
- }
-
- try (SQLiteDatabase db = this.getWritableDatabase()) {
- ContentValues values = new ContentValues();
- values.put(KEY_TIMESTAMP, timestamp);
- values.put(KEY_PROVIDER, provider);
- values.put(KEY_INTENSITY, intensity);
- values.put(KEY_STEPS, steps);
- values.put(KEY_TYPE, kind);
- values.put(KEY_CUSTOM_SHORT, customShortValue);
-
- db.insert(TABLE_GBACTIVITYSAMPLES, null, values);
- }
+ new SchemaMigration(UPDATER_CLASS_NAME_PREFIX).onDowngrade(db, oldVersion, newVersion);
}
@Override
- public void addGBActivitySamples(ActivitySample[] activitySamples) {
- try (SQLiteDatabase db = this.getWritableDatabase()) {
-
- String sql = "INSERT INTO " + TABLE_GBACTIVITYSAMPLES + " (" + KEY_TIMESTAMP + "," +
- KEY_PROVIDER + "," + KEY_INTENSITY + "," + KEY_STEPS + "," + KEY_TYPE + "," + KEY_CUSTOM_SHORT + ")" +
- " VALUES (?,?,?,?,?,?);";
- SQLiteStatement statement = db.compileStatement(sql);
- db.beginTransaction();
-
- for (ActivitySample activitySample : activitySamples) {
- statement.clearBindings();
- statement.bindLong(1, activitySample.getTimestamp());
- statement.bindLong(2, activitySample.getProvider().getID());
- statement.bindLong(3, activitySample.getRawIntensity());
- statement.bindLong(4, activitySample.getSteps());
- statement.bindLong(5, activitySample.getRawKind());
- statement.bindLong(6, activitySample.getCustomValue());
- statement.execute();
- }
- db.setTransactionSuccessful();
- db.endTransaction();
- }
+ public SQLiteDatabase getDatabase() {
+ return super.getWritableDatabase();
}
- public ArrayList getSleepSamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
- return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP, provider);
+ @Override
+ public void closeDb() {
}
- public ArrayList getActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
- return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY, provider);
+ @Override
+ public void openDb() {
}
@Override
@@ -188,120 +70,36 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl
return this;
}
- @Override
- public void release() {
- GBApplication.releaseDB();
+ public Context getContext() {
+ return context;
}
- public ArrayList getAllActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
- return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL, provider);
- }
+ public boolean hasContent() {
+ File dbFile = getContext().getDatabasePath(getDatabaseName());
+ if (dbFile == null || !dbFile.exists()) {
+ return false;
+ }
- /**
- * Returns all available activity samples from between the two timestamps (inclusive), of the given
- * provided and type(s).
- *
- * @param timestamp_from
- * @param timestamp_to
- * @param activityTypes ORed combination of #TYPE_DEEP_SLEEP, #TYPE_LIGHT_SLEEP, #TYPE_ACTIVITY
- * @param provider the producer of the samples to be sought
- * @return
- */
- private ArrayList getGBActivitySamples(int timestamp_from, int timestamp_to, int activityTypes, SampleProvider provider) {
- if (timestamp_to < 0) {
- throw new IllegalArgumentException("negative timestamp_to");
- }
- if (timestamp_from < 0) {
- throw new IllegalArgumentException("negative timestamp_from");
- }
- ArrayList samples = new ArrayList<>();
- final String where = "(provider=" + provider.getID() + " and timestamp>=" + timestamp_from + " and timestamp<=" + timestamp_to + getWhereClauseFor(activityTypes, provider) + ")";
- LOG.info("Activity query where: " + where);
- final String order = "timestamp";
- try (SQLiteDatabase db = this.getReadableDatabase()) {
- try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, null, where, null, null, null, order)) {
- LOG.info("Activity query result: " + cursor.getCount() + " samples");
- int colTimeStamp = cursor.getColumnIndex(KEY_TIMESTAMP);
- int colIntensity = cursor.getColumnIndex(KEY_INTENSITY);
- int colSteps = cursor.getColumnIndex(KEY_STEPS);
- int colType = cursor.getColumnIndex(KEY_TYPE);
- int colCustomShort = cursor.getColumnIndex(KEY_CUSTOM_SHORT);
- while (cursor.moveToNext()) {
- GBActivitySample sample = new GBActivitySample(
- provider,
- cursor.getInt(colTimeStamp),
- cursor.getInt(colIntensity),
- cursor.getInt(colSteps),
- cursor.getInt(colType),
- cursor.getInt(colCustomShort));
- samples.add(sample);
+ try {
+ try (SQLiteDatabase db = this.getReadableDatabase()) {
+ try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, new String[]{KEY_TIMESTAMP}, null, null, null, null, null, "1")) {
+ return cursor.moveToFirst();
}
}
- }
-
- return samples;
- }
-
- private String getWhereClauseFor(int activityTypes, SampleProvider provider) {
- if (activityTypes == ActivityKind.TYPE_ALL) {
- return ""; // no further restriction
- }
-
- StringBuilder builder = new StringBuilder(" and (");
- int[] dbActivityTypes = ActivityKind.mapToDBActivityTypes(activityTypes, provider);
- for (int i = 0; i < dbActivityTypes.length; i++) {
- builder.append(" type=").append(dbActivityTypes[i]);
- if (i + 1 < dbActivityTypes.length) {
- builder.append(" or ");
- }
- }
- builder.append(')');
- return builder.toString();
- }
-
- @Override
- public void changeStoredSamplesType(int timestampFrom, int timestampTo, int kind, SampleProvider provider) {
- try (SQLiteDatabase db = this.getReadableDatabase()) {
- String sql = "UPDATE " + TABLE_GBACTIVITYSAMPLES + " SET " + KEY_TYPE + "= ? WHERE "
- + KEY_PROVIDER + " = ? AND "
- + KEY_TIMESTAMP + " >= ? AND " + KEY_TIMESTAMP + " < ? ;"; //do not use BETWEEN because the range is inclusive in that case!
-
- SQLiteStatement statement = db.compileStatement(sql);
- statement.bindLong(1, kind);
- statement.bindLong(2, provider.getID());
- statement.bindLong(3, timestampFrom);
- statement.bindLong(4, timestampTo);
- statement.execute();
+ } catch (Exception ex) {
+ // can't expect anything
+ GB.log("Error looking for old activity data: " + ex.getMessage(), GB.ERROR, ex);
+ return false;
}
}
@Override
- public void changeStoredSamplesType(int timestampFrom, int timestampTo, int fromKind, int toKind, SampleProvider provider) {
- try (SQLiteDatabase db = this.getReadableDatabase()) {
- String sql = "UPDATE " + TABLE_GBACTIVITYSAMPLES + " SET " + KEY_TYPE + "= ? WHERE "
- + KEY_TYPE + " = ? AND "
- + KEY_PROVIDER + " = ? AND "
- + KEY_TIMESTAMP + " >= ? AND " + KEY_TIMESTAMP + " < ? ;"; //do not use BETWEEN because the range is inclusive in that case!
-
- SQLiteStatement statement = db.compileStatement(sql);
- statement.bindLong(1, toKind);
- statement.bindLong(2, fromKind);
- statement.bindLong(3, provider.getID());
- statement.bindLong(4, timestampFrom);
- statement.bindLong(5, timestampTo);
- statement.execute();
- }
+ public DaoSession getDaoSession() {
+ throw new UnsupportedOperationException();
}
@Override
- public int fetchLatestTimestamp(SampleProvider provider) {
- try (SQLiteDatabase db = this.getReadableDatabase()) {
- try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, new String[]{KEY_TIMESTAMP}, KEY_PROVIDER + "=" + String.valueOf(provider.getID()), null, null, null, KEY_TIMESTAMP + " DESC", "1")) {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- }
- }
- return -1;
+ public DaoMaster getDaoMaster() {
+ throw new UnsupportedOperationException();
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java
index ed42b28db..4504393a8 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java
@@ -26,16 +26,10 @@ public abstract class DBAccess extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
- DBHandler handler = null;
- try {
- handler = GBApplication.acquireDB();
- doInBackground(handler);
+ try (DBHandler db = GBApplication.acquireDB()) {
+ doInBackground(db);
} catch (Exception e) {
mError = e;
- } finally {
- if (handler != null) {
- handler.release();
- }
}
return null;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java
index 5d1a72b4d..671b5af38 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java
@@ -1,10 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.database;
+/**
+ * TODO: Legacy, can be removed once migration support for old ActivityDatabase is removed
+ * @deprecated only for backwards compatibility
+ */
public class DBConstants {
public static final String DATABASE_NAME = "ActivityDatabase";
public static final String TABLE_GBACTIVITYSAMPLES = "GBActivitySamples";
- public static final String TABLE_STEPS_PER_DAY = "StepsPerDay";
public static final String KEY_TIMESTAMP = "timestamp";
public static final String KEY_PROVIDER = "provider";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java
index 49dbbd400..d4f091252 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java
@@ -3,37 +3,34 @@ package nodomain.freeyourgadget.gadgetbridge.database;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
-import java.util.List;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
-import nodomain.freeyourgadget.gadgetbridge.GBApplication;
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+/**
+ * Provides lowlevel access to the database.
+ */
+public interface DBHandler extends AutoCloseable {
+ /**
+ * Closes the database.
+ */
+ void closeDb();
+
+ /**
+ * Opens the database. Note that this is only possible after an explicit
+ * #closeDb(). Initially the db is implicitly open.
+ */
+ void openDb();
-public interface DBHandler {
SQLiteOpenHelper getHelper();
/**
- * Releases the DB handler. No access may be performed after calling this method.
- * Same as calling {@link GBApplication#releaseDB()}
+ * Releases the DB handler. No DB access will be possible before
+ * #openDb() will be called.
*/
- void release();
+ void close() throws Exception;
- List getAllActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
-
- List getActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
-
- List getSleepSamples(int tsFrom, int tsTo, SampleProvider provider);
-
- void addGBActivitySample(int timestamp, int provider, int intensity, int steps, int kind, int heartrate);
-
- void addGBActivitySamples(ActivitySample[] activitySamples);
-
- SQLiteDatabase getWritableDatabase();
-
- void changeStoredSamplesType(int timestampFrom, int timestampTo, int kind, SampleProvider provider);
-
- void changeStoredSamplesType(int timestampFrom, int timestampTo, int fromKind, int toKind, SampleProvider provider);
-
- int fetchLatestTimestamp(SampleProvider provider);
+ SQLiteDatabase getDatabase();
+ DaoMaster getDaoMaster();
+ DaoSession getDaoSession();
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
index 7c53d3265..35f0efbf7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
@@ -4,58 +4,132 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
import java.util.Date;
+import java.util.List;
import java.util.Locale;
+import java.util.Objects;
+import de.greenrobot.dao.Property;
+import de.greenrobot.dao.query.Query;
+import de.greenrobot.dao.query.QueryBuilder;
+import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHealthSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMisfitSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescription;
+import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescriptionDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes;
+import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlay;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.Tag;
+import nodomain.freeyourgadget.gadgetbridge.entities.TagDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.entities.UserAttributes;
+import nodomain.freeyourgadget.gadgetbridge.entities.UserDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
+import nodomain.freeyourgadget.gadgetbridge.model.ValidByDate;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_CUSTOM_SHORT;
+import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY;
+import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
+import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
+import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
+import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
+
+/**
+ * Provides utiliy access to some common entities, so you won't need to use
+ * their DAO classes.
+ *
+ * Maybe this code should actually be in the DAO classes themselves, but then
+ * these should be under revision control instead of 100% generated at build time.
+ */
public class DBHelper {
+ private static final Logger LOG = LoggerFactory.getLogger(DBHelper.class);
+
private final Context context;
public DBHelper(Context context) {
this.context = context;
}
- private String getClosedDBPath(SQLiteOpenHelper dbHandler) throws IllegalStateException {
- SQLiteDatabase db = dbHandler.getReadableDatabase();
+ /**
+ * Closes the database and returns its name.
+ * Important: after calling this, you have to DBHandler#openDb() it again
+ * to get it back to work.
+ *
+ * @param dbHandler
+ * @return
+ * @throws IllegalStateException
+ */
+ private String getClosedDBPath(DBHandler dbHandler) throws IllegalStateException {
+ SQLiteDatabase db = dbHandler.getDatabase();
String path = db.getPath();
- db.close();
+ dbHandler.closeDb();
if (db.isOpen()) { // reference counted, so may still be open
throw new IllegalStateException("Database must be closed");
}
return path;
}
- public File exportDB(SQLiteOpenHelper dbHandler, File toDir) throws IllegalStateException, IOException {
+ public File exportDB(DBHandler dbHandler, File toDir) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
- File sourceFile = new File(dbPath);
- File destFile = new File(toDir, sourceFile.getName());
- if (destFile.exists()) {
- File backup = new File(toDir, destFile.getName() + "_" + getDate());
- destFile.renameTo(backup);
- } else if (!toDir.exists()) {
- if (!toDir.mkdirs()) {
- throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
+ try {
+ File sourceFile = new File(dbPath);
+ File destFile = new File(toDir, sourceFile.getName());
+ if (destFile.exists()) {
+ File backup = new File(toDir, destFile.getName() + "_" + getDate());
+ destFile.renameTo(backup);
+ } else if (!toDir.exists()) {
+ if (!toDir.mkdirs()) {
+ throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
+ }
}
- }
- FileUtils.copyFile(sourceFile, destFile);
- return destFile;
+ FileUtils.copyFile(sourceFile, destFile);
+ return destFile;
+ } finally {
+ dbHandler.openDb();
+ }
}
private String getDate() {
return new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
}
- public void importDB(SQLiteOpenHelper dbHandler, File fromFile) throws IllegalStateException, IOException {
+ public void importDB(DBHandler dbHandler, File fromFile) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
- File toFile = new File(dbPath);
- FileUtils.copyFile(fromFile, toFile);
+ try {
+ File toFile = new File(dbPath);
+ FileUtils.copyFile(fromFile, toFile);
+ } finally {
+ dbHandler.openDb();
+ }
}
public void validateDB(SQLiteOpenHelper dbHandler) throws IOException {
@@ -71,6 +145,11 @@ public class DBHelper {
db.execSQL(statement);
}
+ public boolean existsDB(String dbName) {
+ File path = context.getDatabasePath(dbName);
+ return path != null && path.exists();
+ }
+
public static boolean existsColumn(String tableName, String columnName, SQLiteDatabase db) {
try (Cursor res = db.rawQuery("PRAGMA table_info('" + tableName + "')", null)) {
int index = res.getColumnIndex("name");
@@ -93,10 +172,530 @@ public class DBHelper {
*
* @return the "WITHOUT ROWID" string or an empty string for pre-Lollipop devices
*/
+ @NonNull
public static String getWithoutRowId() {
if (GBApplication.isRunningLollipopOrLater()) {
return " WITHOUT ROWID;";
}
return "";
}
+
+ /**
+ * Looks up the user entity in the database. If a user exists already, it will
+ * be updated with the current preferences values. If no user exists yet, it will
+ * be created in the database.
+ *
+ * Note: so far there is only ever a single user; there is no multi-user support yet
+ * @param session
+ * @return the User entity
+ */
+ @NonNull
+ public static User getUser(DaoSession session) {
+ ActivityUser prefsUser = new ActivityUser();
+ UserDao userDao = session.getUserDao();
+ User user;
+ List users = userDao.loadAll();
+ if (users.isEmpty()) {
+ user = createUser(prefsUser, session);
+ } else {
+ user = users.get(0); // TODO: multiple users support?
+ ensureUserUpToDate(user, prefsUser, session);
+ }
+ ensureUserAttributes(user, prefsUser, session);
+
+ return user;
+ }
+
+ @NonNull
+ public static UserAttributes getUserAttributes(User user) {
+ List list = user.getUserAttributesList();
+ if (list.isEmpty()) {
+ throw new IllegalStateException("user has no attributes");
+ }
+ return list.get(0);
+ }
+
+ @NonNull
+ private static User createUser(ActivityUser prefsUser, DaoSession session) {
+ User user = new User();
+ ensureUserUpToDate(user, prefsUser, session);
+
+ return user;
+ }
+
+ private static void ensureUserUpToDate(User user, ActivityUser prefsUser, DaoSession session) {
+ if (!isUserUpToDate(user, prefsUser)) {
+ user.setName(prefsUser.getName());
+ user.setBirthday(prefsUser.getUserBirthday());
+ user.setGender(prefsUser.getGender());
+
+ if (user.getId() == null) {
+ session.getUserDao().insert(user);
+ } else {
+ session.getUserDao().update(user);
+ }
+ }
+ }
+
+ public static boolean isUserUpToDate(User user, ActivityUser prefsUser) {
+ if (!Objects.equals(user.getName(), prefsUser.getName())) {
+ return false;
+ }
+ if (!Objects.equals(user.getBirthday(), prefsUser.getUserBirthday())) {
+ return false;
+ }
+ if (user.getGender() != prefsUser.getGender()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static void ensureUserAttributes(User user, ActivityUser prefsUser, DaoSession session) {
+ List userAttributes = user.getUserAttributesList();
+ UserAttributes[] previousUserAttributes = new UserAttributes[1];
+ if (hasUpToDateUserAttributes(userAttributes, prefsUser, previousUserAttributes)) {
+ return;
+ }
+
+ Calendar now = DateTimeUtils.getCalendarUTC();
+ invalidateUserAttributes(previousUserAttributes[0], now, session);
+
+ UserAttributes attributes = new UserAttributes();
+ attributes.setValidFromUTC(now.getTime());
+ attributes.setHeightCM(prefsUser.getHeightCm());
+ attributes.setWeightKG(prefsUser.getWeightKg());
+ attributes.setSleepGoalHPD(prefsUser.getSleepDuration());
+ attributes.setStepsGoalSPD(prefsUser.getStepsGoal());
+ attributes.setUserId(user.getId());
+ session.getUserAttributesDao().insert(attributes);
+
+// sort order is important, so we re-fetch from the db
+// userAttributes.add(attributes);
+ user.resetUserAttributesList();
+ }
+
+ private static void invalidateUserAttributes(UserAttributes userAttributes, Calendar now, DaoSession session) {
+ if (userAttributes != null) {
+ Calendar invalid = (Calendar) now.clone();
+ invalid.add(Calendar.MINUTE, -1);
+ userAttributes.setValidToUTC(invalid.getTime());
+ session.getUserAttributesDao().update(userAttributes);
+ }
+ }
+
+ private static boolean hasUpToDateUserAttributes(List userAttributes, ActivityUser prefsUser, UserAttributes[] outPreviousUserAttributes) {
+ for (UserAttributes attr : userAttributes) {
+ if (!isValidNow(attr)) {
+ continue;
+ }
+ if (isEqual(attr, prefsUser)) {
+ return true;
+ } else {
+ outPreviousUserAttributes[0] = attr;
+ }
+ }
+ return false;
+ }
+
+ // TODO: move this into db queries?
+ private static boolean isValidNow(ValidByDate element) {
+ Calendar cal = DateTimeUtils.getCalendarUTC();
+ Date nowUTC = cal.getTime();
+ return isValid(element, nowUTC);
+ }
+
+ private static boolean isValid(ValidByDate element, Date nowUTC) {
+ Date validFromUTC = element.getValidFromUTC();
+ Date validToUTC = element.getValidToUTC();
+ if (nowUTC.before(validFromUTC)) {
+ return false;
+ }
+ if (validToUTC != null && nowUTC.after(validToUTC)) {
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean isEqual(UserAttributes attr, ActivityUser prefsUser) {
+ if (prefsUser.getHeightCm() != attr.getHeightCM()) {
+ LOG.info("user height changed to " + prefsUser.getHeightCm() + " from " + attr.getHeightCM());
+ return false;
+ }
+ if (prefsUser.getWeightKg() != attr.getWeightKG()) {
+ LOG.info("user changed to " + prefsUser.getWeightKg() + " from " + attr.getWeightKG());
+ return false;
+ }
+ if (!Integer.valueOf(prefsUser.getSleepDuration()).equals(attr.getSleepGoalHPD())) {
+ LOG.info("user sleep goal changed to " + prefsUser.getSleepDuration() + " from " + attr.getSleepGoalHPD());
+ return false;
+ }
+ if (!Integer.valueOf(prefsUser.getStepsGoal()).equals(attr.getStepsGoalSPD())) {
+ LOG.info("user steps goal changed to " + prefsUser.getStepsGoal() + " from " + attr.getStepsGoalSPD());
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean isEqual(DeviceAttributes attr, GBDevice gbDevice) {
+ if (!Objects.equals(attr.getFirmwareVersion1(), gbDevice.getFirmwareVersion())) {
+ return false;
+ }
+ if (!Objects.equals(attr.getFirmwareVersion2(), gbDevice.getFirmwareVersion2())) {
+ return false;
+ }
+ if (!Objects.equals(attr.getVolatileIdentifier(), gbDevice.getVolatileAddress())) {
+ return false;
+ }
+ return true;
+ }
+
+ public static Device findDevice(GBDevice gbDevice, DaoSession session) {
+ DeviceDao deviceDao = session.getDeviceDao();
+ Query query = deviceDao.queryBuilder().where(DeviceDao.Properties.Identifier.eq(gbDevice.getAddress())).build();
+ List devices = query.list();
+ if (devices.size() > 0) {
+ return devices.get(0);
+ }
+ return null;
+ }
+
+ /**
+ * Returns all active (that is, not old, archived ones) from the database.
+ * (currently the active handling is not available)
+ * @param daoSession
+ */
+ public static List getActiveDevices(DaoSession daoSession) {
+ return daoSession.getDeviceDao().loadAll();
+ }
+
+ /**
+ * Looks up in the database the Device entity corresponding to the GBDevice. If a device
+ * exists already, it will be updated with the current preferences values. If no device exists
+ * yet, it will be created in the database.
+ *
+ * @param session
+ * @return the device entity corresponding to the given GBDevice
+ */
+ public static Device getDevice(GBDevice gbDevice, DaoSession session) {
+ Device device = findDevice(gbDevice, session);
+ if (device == null) {
+ device = createDevice(gbDevice, session);
+ } else {
+ ensureDeviceUpToDate(device, gbDevice, session);
+ }
+ ensureDeviceAttributes(device, gbDevice, session);
+
+ return device;
+ }
+
+ @NonNull
+ public static DeviceAttributes getDeviceAttributes(Device device) {
+ List list = device.getDeviceAttributesList();
+ if (list.isEmpty()) {
+ throw new IllegalStateException("device has no attributes");
+ }
+ return list.get(0);
+ }
+
+ private static void ensureDeviceUpToDate(Device device, GBDevice gbDevice, DaoSession session) {
+ if (!isDeviceUpToDate(device, gbDevice)) {
+ device.setIdentifier(gbDevice.getAddress());
+ device.setName(gbDevice.getName());
+ DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+ device.setManufacturer(coordinator.getManufacturer());
+ device.setType(gbDevice.getType().getKey());
+ device.setModel(gbDevice.getModel());
+
+ if (device.getId() == null) {
+ session.getDeviceDao().insert(device);
+ } else {
+ session.getDeviceDao().update(device);
+ }
+ }
+ }
+
+ private static boolean isDeviceUpToDate(Device device, GBDevice gbDevice) {
+ if (!Objects.equals(device.getIdentifier(), gbDevice.getAddress())) {
+ return false;
+ }
+ if (!Objects.equals(device.getName(), gbDevice.getName())) {
+ return false;
+ }
+ DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+ if (!Objects.equals(device.getManufacturer(), coordinator.getManufacturer())) {
+ return false;
+ }
+ if (device.getType() != gbDevice.getType().getKey()) {
+ return false;
+ }
+ if (!Objects.equals(device.getModel(), gbDevice.getModel())) {
+ return false;
+ }
+ return true;
+ }
+
+ private static Device createDevice(GBDevice gbDevice, DaoSession session) {
+ Device device = new Device();
+ ensureDeviceUpToDate(device, gbDevice, session);
+
+ return device;
+ }
+
+ private static void ensureDeviceAttributes(Device device, GBDevice gbDevice, DaoSession session) {
+ List deviceAttributes = device.getDeviceAttributesList();
+ DeviceAttributes[] previousDeviceAttributes = new DeviceAttributes[1];
+ if (hasUpToDateDeviceAttributes(deviceAttributes, gbDevice, previousDeviceAttributes)) {
+ return;
+ }
+
+ Calendar now = DateTimeUtils.getCalendarUTC();
+ invalidateDeviceAttributes(previousDeviceAttributes[0], now, session);
+
+ DeviceAttributes attributes = new DeviceAttributes();
+ attributes.setDeviceId(device.getId());
+ attributes.setValidFromUTC(now.getTime());
+ attributes.setFirmwareVersion1(gbDevice.getFirmwareVersion());
+ attributes.setFirmwareVersion2(gbDevice.getFirmwareVersion2());
+ attributes.setVolatileIdentifier(gbDevice.getVolatileAddress());
+ DeviceAttributesDao attributesDao = session.getDeviceAttributesDao();
+ attributesDao.insert(attributes);
+
+// sort order is important, so we re-fetch from the db
+// deviceAttributes.add(attributes);
+ device.resetDeviceAttributesList();
+ }
+
+ private static void invalidateDeviceAttributes(DeviceAttributes deviceAttributes, Calendar now, DaoSession session) {
+ if (deviceAttributes != null) {
+ Calendar invalid = (Calendar) now.clone();
+ invalid.add(Calendar.MINUTE, -1);
+ deviceAttributes.setValidToUTC(invalid.getTime());
+ session.getDeviceAttributesDao().update(deviceAttributes);
+ }
+ }
+
+ private static boolean hasUpToDateDeviceAttributes(List deviceAttributes, GBDevice gbDevice, DeviceAttributes[] outPreviousAttributes) {
+ for (DeviceAttributes attr : deviceAttributes) {
+ if (!isValidNow(attr)) {
+ continue;
+ }
+ if (isEqual(attr, gbDevice)) {
+ return true;
+ } else {
+ outPreviousAttributes[0] = attr;
+ }
+ }
+ return false;
+ }
+
+ @NonNull
+ public static List findActivityDecriptions(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
+ Property tsFromProperty = ActivityDescriptionDao.Properties.TimestampFrom;
+ Property tsToProperty = ActivityDescriptionDao.Properties.TimestampTo;
+ Property userIdProperty = ActivityDescriptionDao.Properties.UserId;
+ QueryBuilder qb = session.getActivityDescriptionDao().queryBuilder();
+ qb.where(userIdProperty.eq(user.getId()), isAtLeastPartiallyInRange(qb, tsFromProperty, tsToProperty, tsFrom, tsTo));
+ List descriptions = qb.build().list();
+ return descriptions;
+ }
+
+ /**
+ * Returns a condition that matches when the range of the entity (tsFromProperty..tsToProperty)
+ * is completely or partially inside the range tsFrom..tsTo.
+ * @param qb the query builder to use
+ * @param tsFromProperty the property indicating the start of the entity's range
+ * @param tsToProperty the property indicating the end of the entity's range
+ * @param tsFrom the timestamp indicating the start of the range to match
+ * @param tsTo the timestamp indicating the end of the range to match
+ * @param the query builder's type parameter
+ * @return the range WhereCondition
+ */
+ private static WhereCondition isAtLeastPartiallyInRange(QueryBuilder qb, Property tsFromProperty, Property tsToProperty, int tsFrom, int tsTo) {
+ return qb.and(tsFromProperty.lt(tsTo), tsToProperty.gt(tsFrom));
+ }
+
+ @NonNull
+ public static ActivityDescription createActivityDescription(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
+ ActivityDescription desc = new ActivityDescription();
+ desc.setUser(user);
+ desc.setTimestampFrom(tsFrom);
+ desc.setTimestampTo(tsTo);
+ session.getActivityDescriptionDao().insertOrReplace(desc);
+
+ return desc;
+ }
+
+ @NonNull
+ public static Tag getTag(@NonNull User user, @NonNull String name, @NonNull DaoSession session) {
+ TagDao tagDao = session.getTagDao();
+ QueryBuilder qb = tagDao.queryBuilder();
+ Query query = qb.where(TagDao.Properties.UserId.eq(user.getId()), TagDao.Properties.Name.eq(name)).build();
+ List tags = query.list();
+ if (tags.size() > 0) {
+ return tags.get(0);
+ }
+ return createTag(user, name, null, session);
+ }
+
+ static Tag createTag(@NonNull User user, @NonNull String name, @Nullable String description, @NonNull DaoSession session) {
+ Tag tag = new Tag();
+ tag.setUserId(user.getId());
+ tag.setName(name);
+ tag.setDescription(description);
+ session.getTagDao().insertOrReplace(tag);
+ return tag;
+ }
+
+ /**
+ * Returns the old activity database handler if there is any content in that
+ * db, or null otherwise.
+ *
+ * @return the old activity db handler or null
+ */
+ @Nullable
+ public ActivityDatabaseHandler getOldActivityDatabaseHandler() {
+ ActivityDatabaseHandler handler = new ActivityDatabaseHandler(context);
+ if (handler.hasContent()) {
+ return handler;
+ }
+ return null;
+ }
+
+ public void importOldDb(ActivityDatabaseHandler oldDb, GBDevice targetDevice, DBHandler targetDBHandler) {
+ DaoSession tempSession = targetDBHandler.getDaoMaster().newSession();
+ try {
+ importActivityDatabase(oldDb, targetDevice, tempSession);
+ } finally {
+ tempSession.clear();
+ }
+ }
+
+ private boolean isEmpty(DaoSession session) {
+ long totalSamplesCount = session.getMiBandActivitySampleDao().count();
+ totalSamplesCount += session.getPebbleHealthActivitySampleDao().count();
+ return totalSamplesCount == 0;
+ }
+
+ private void importActivityDatabase(ActivityDatabaseHandler oldDbHandler, GBDevice targetDevice, DaoSession session) {
+ try (SQLiteDatabase oldDB = oldDbHandler.getReadableDatabase()) {
+ User user = DBHelper.getUser(session);
+ for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
+ if (coordinator.supports(targetDevice)) {
+ AbstractSampleProvider extends AbstractActivitySample> sampleProvider = (AbstractSampleProvider extends AbstractActivitySample>) coordinator.getSampleProvider(targetDevice, session);
+ importActivitySamples(oldDB, targetDevice, session, sampleProvider, user);
+ break;
+ }
+ }
+ }
+ }
+
+ private void importActivitySamples(SQLiteDatabase fromDb, GBDevice targetDevice, DaoSession targetSession, AbstractSampleProvider sampleProvider, User user) {
+ if (sampleProvider instanceof PebbleMisfitSampleProvider) {
+ GB.toast(context, "Migration of old Misfit data is not supported!", Toast.LENGTH_LONG, GB.WARN);
+ return;
+ }
+
+ String order = "timestamp";
+ final String where = "provider=" + sampleProvider.getID();
+
+ boolean convertActivityTypeToRange = false;
+ int currentTypeRun, previousTypeRun, currentTimeStamp, currentTypeStartTimeStamp, currentTypeEndTimeStamp;
+ List overlayList = new ArrayList<>();
+
+ final int BATCH_SIZE = 100000; // 100.000 samples = rougly 20 MB per batch
+ List newSamples;
+ if (sampleProvider instanceof PebbleHealthSampleProvider) {
+ convertActivityTypeToRange = true;
+ previousTypeRun = ActivitySample.NOT_MEASURED;
+ currentTypeStartTimeStamp = -1;
+ currentTypeEndTimeStamp = -1;
+
+ } else {
+ previousTypeRun = currentTypeStartTimeStamp = currentTypeEndTimeStamp = 0;
+ }
+ try (Cursor cursor = fromDb.query(TABLE_GBACTIVITYSAMPLES, null, where, null, null, null, order)) {
+ int colTimeStamp = cursor.getColumnIndex(KEY_TIMESTAMP);
+ int colIntensity = cursor.getColumnIndex(KEY_INTENSITY);
+ int colSteps = cursor.getColumnIndex(KEY_STEPS);
+ int colType = cursor.getColumnIndex(KEY_TYPE);
+ int colCustomShort = cursor.getColumnIndex(KEY_CUSTOM_SHORT);
+ long deviceId = DBHelper.getDevice(targetDevice, targetSession).getId();
+ long userId = user.getId();
+ newSamples = new ArrayList<>(Math.min(BATCH_SIZE, cursor.getCount()));
+ while (cursor.moveToNext()) {
+ T newSample = sampleProvider.createActivitySample();
+ newSample.setProvider(sampleProvider);
+ newSample.setUserId(userId);
+ newSample.setDeviceId(deviceId);
+ currentTimeStamp = cursor.getInt(colTimeStamp);
+ newSample.setTimestamp(currentTimeStamp);
+ newSample.setRawIntensity(getNullableInt(cursor, colIntensity, ActivitySample.NOT_MEASURED));
+ currentTypeRun = getNullableInt(cursor, colType, ActivitySample.NOT_MEASURED);
+ newSample.setRawKind(currentTypeRun);
+ if (convertActivityTypeToRange) {
+ //at the beginning there is no start timestamp
+ if (currentTypeStartTimeStamp == -1) {
+ currentTypeStartTimeStamp = currentTypeEndTimeStamp = currentTimeStamp;
+ previousTypeRun = currentTypeRun;
+ }
+
+ if (currentTypeRun != previousTypeRun) {
+ //we used not to store the last sample, now we do the opposite and we need to round up
+ currentTypeEndTimeStamp = currentTimeStamp;
+ //if the Type has changed, the run has ended. Only store light and deep sleep data
+ if (previousTypeRun == 4) {
+ overlayList.add(new PebbleHealthActivityOverlay(currentTypeStartTimeStamp, currentTypeEndTimeStamp, sampleProvider.toRawActivityKind(ActivityKind.TYPE_LIGHT_SLEEP), deviceId, userId, null));
+ } else if (previousTypeRun == 5) {
+ overlayList.add(new PebbleHealthActivityOverlay(currentTypeStartTimeStamp, currentTypeEndTimeStamp, sampleProvider.toRawActivityKind(ActivityKind.TYPE_DEEP_SLEEP), deviceId, userId, null));
+ }
+ currentTypeStartTimeStamp = currentTimeStamp;
+ previousTypeRun = currentTypeRun;
+ } else {
+ //just expand the run
+ currentTypeEndTimeStamp = currentTimeStamp;
+ }
+
+ }
+ newSample.setSteps(getNullableInt(cursor, colSteps, ActivitySample.NOT_MEASURED));
+ if (colCustomShort > -1) {
+ newSample.setHeartRate(getNullableInt(cursor, colCustomShort, ActivitySample.NOT_MEASURED));
+ } else {
+ newSample.setHeartRate(ActivitySample.NOT_MEASURED);
+ }
+ newSamples.add(newSample);
+
+ if ((newSamples.size() % BATCH_SIZE) == 0) {
+ sampleProvider.getSampleDao().insertOrReplaceInTx(newSamples, true);
+ targetSession.clear();
+ newSamples.clear();
+ }
+ }
+ // and insert the remaining samples
+ if (!newSamples.isEmpty()) {
+ sampleProvider.getSampleDao().insertOrReplaceInTx(newSamples, true);
+ }
+ // store the overlay records
+ if (!overlayList.isEmpty()) {
+ PebbleHealthActivityOverlayDao overlayDao = targetSession.getPebbleHealthActivityOverlayDao();
+ overlayDao.insertOrReplaceInTx(overlayList);
+ }
+ }
+ }
+
+ private int getNullableInt(Cursor cursor, int columnIndex, int defaultValue) {
+ if (cursor.isNull(columnIndex)) {
+ return defaultValue;
+ }
+ return cursor.getInt(columnIndex);
+ }
+
+ public static void clearSession() {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DaoSession session = dbHandler.getDaoSession();
+ session.clear();
+ } catch (Exception e) {
+ LOG.warn("Unable to acquire database to clear the session", e);
+ }
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBOpenHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBOpenHelper.java
new file mode 100644
index 000000000..9a61ae7b2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBOpenHelper.java
@@ -0,0 +1,34 @@
+package nodomain.freeyourgadget.gadgetbridge.database;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+
+import nodomain.freeyourgadget.gadgetbridge.database.schema.SchemaMigration;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
+
+public class DBOpenHelper extends DaoMaster.OpenHelper {
+ private final String updaterClassNamePrefix;
+ private final Context context;
+
+ public DBOpenHelper(Context context, String dbName, SQLiteDatabase.CursorFactory factory) {
+ super(context, dbName, factory);
+ updaterClassNamePrefix = dbName + "Update_";
+ this.context = context;
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ DaoMaster.createAllTables(db, true);
+ new SchemaMigration(updaterClassNamePrefix).onUpgrade(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ DaoMaster.createAllTables(db, true);
+ new SchemaMigration(updaterClassNamePrefix).onDowngrade(db, oldVersion, newVersion);
+ }
+
+ public Context getContext() {
+ return context;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java
deleted file mode 100644
index e23d13e50..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.database.schema;
-
-import android.database.sqlite.SQLiteDatabase;
-
-import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
-import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
-
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
-import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_STEPS_PER_DAY;
-
-/**
- * Adds a table "STEPS_PER_DAY".
- */
-public class ActivityDBUpdate_X implements DBUpdateScript {
- @Override
- public void upgradeSchema(SQLiteDatabase db) {
- String CREATE_STEPS_PER_DAY_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_STEPS_PER_DAY + " ("
- + KEY_TIMESTAMP + " INT,"
- + KEY_PROVIDER + " TINYINT,"
- + KEY_STEPS + " MEDIUMINT,"
- + " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId();
- db.execSQL(CREATE_STEPS_PER_DAY_TABLE);
- }
-
- @Override
- public void downgradeSchema(SQLiteDatabase db) {
- DBHelper.dropTable(TABLE_STEPS_PER_DAY, db);
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_14.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_14.java
new file mode 100644
index 000000000..f7c64ebe8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_14.java
@@ -0,0 +1,26 @@
+package nodomain.freeyourgadget.gadgetbridge.database.schema;
+
+import android.database.sqlite.SQLiteDatabase;
+
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
+
+/*
+ * adds heart rate column to health table
+ */
+
+public class GadgetbridgeUpdate_14 implements DBUpdateScript {
+ @Override
+ public void upgradeSchema(SQLiteDatabase db) {
+ if (!DBHelper.existsColumn(PebbleHealthActivitySampleDao.TABLENAME, PebbleHealthActivitySampleDao.Properties.HeartRate.columnName, db)) {
+ String ADD_COLUMN_HEART_RATE = "ALTER TABLE " + PebbleHealthActivitySampleDao.TABLENAME + " ADD COLUMN "
+ + PebbleHealthActivitySampleDao.Properties.HeartRate.columnName + " INTEGER NOT NULL DEFAULT 0;";
+ db.execSQL(ADD_COLUMN_HEART_RATE);
+ }
+ }
+
+ @Override
+ public void downgradeSchema(SQLiteDatabase db) {
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_15.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_15.java
new file mode 100644
index 000000000..615f1b8d2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_15.java
@@ -0,0 +1,26 @@
+package nodomain.freeyourgadget.gadgetbridge.database.schema;
+
+import android.database.sqlite.SQLiteDatabase;
+
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
+import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
+
+/*
+ * adds heart rate column to health table
+ */
+
+public class GadgetbridgeUpdate_15 implements DBUpdateScript {
+ @Override
+ public void upgradeSchema(SQLiteDatabase db) {
+ if (!DBHelper.existsColumn(DeviceAttributesDao.TABLENAME, DeviceAttributesDao.Properties.VolatileIdentifier.columnName, db)) {
+ String ADD_COLUMN_VOLATILE_IDENTIFIER = "ALTER TABLE " + DeviceAttributesDao.TABLENAME + " ADD COLUMN "
+ + DeviceAttributesDao.Properties.VolatileIdentifier.columnName + " TEXT;";
+ db.execSQL(ADD_COLUMN_VOLATILE_IDENTIFIER);
+ }
+ }
+
+ @Override
+ public void downgradeSchema(SQLiteDatabase db) {
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/SchemaMigration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/SchemaMigration.java
new file mode 100644
index 000000000..40c6df152
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/SchemaMigration.java
@@ -0,0 +1,64 @@
+package nodomain.freeyourgadget.gadgetbridge.database.schema;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class SchemaMigration {
+ private static final Logger LOG = LoggerFactory.getLogger(SchemaMigration.class);
+ private final String classNamePrefix;
+
+ public SchemaMigration(String updaterClassNamePrefix) {
+ classNamePrefix = updaterClassNamePrefix;
+ }
+
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ LOG.info("ActivityDatabase: schema upgrade requested from " + oldVersion + " to " + newVersion);
+ try {
+ for (int i = oldVersion + 1; i <= newVersion; i++) {
+ DBUpdateScript updater = getUpdateScript(db, i);
+ if (updater != null) {
+ LOG.info("upgrading activity database to version " + i);
+ updater.upgradeSchema(db);
+ }
+ }
+ LOG.info("activity database is now at version " + newVersion);
+ } catch (RuntimeException ex) {
+ GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
+ throw ex; // reject upgrade
+ }
+ }
+
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ LOG.info("ActivityDatabase: schema downgrade requested from " + oldVersion + " to " + newVersion);
+ try {
+ for (int i = oldVersion; i >= newVersion; i--) {
+ DBUpdateScript updater = getUpdateScript(db, i);
+ if (updater != null) {
+ LOG.info("downgrading activity database to version " + (i - 1));
+ updater.downgradeSchema(db);
+ }
+ }
+ LOG.info("activity database is now at version " + newVersion);
+ } catch (RuntimeException ex) {
+ GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
+ throw ex; // reject downgrade
+ }
+ }
+
+ private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
+ try {
+ Class> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + "." + classNamePrefix + version);
+ return (DBUpdateScript) updateClass.newInstance();
+ } catch (ClassNotFoundException e) {
+ return null;
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java
index 473210391..2fc59fd8a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java
@@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.deviceevents;
public class GBDeviceEventNotificationControl extends GBDeviceEvent {
public int handle;
+ public String phoneNumber;
public String reply;
public Event event = Event.UNKNOWN;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
index 28e5fcf8a..46a65e09c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
@@ -1,9 +1,106 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.support.annotation.NonNull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import de.greenrobot.dao.query.QueryBuilder;
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceCoordinator.class);
+
+ @Override
+ public final boolean supports(GBDeviceCandidate candidate) {
+ return getSupportedType(candidate).isSupported();
+ }
+
+ @Override
+ public boolean supports(GBDevice device) {
+ return getDeviceType().equals(device.getType());
+ }
+
+ @NonNull
+ @Override
+ public Collection extends ScanFilter> createBLEScanFilters() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public GBDevice createDevice(GBDeviceCandidate candidate) {
+ return new GBDevice(candidate.getDevice().getAddress(), candidate.getName(), getDeviceType());
+ }
+
+ @Override
+ public void deleteDevice(final GBDevice gbDevice) throws GBException {
+ LOG.info("will try to delete device: " + gbDevice.getName());
+ if (gbDevice.isConnected() || gbDevice.isConnecting()) {
+ GBApplication.deviceService().disconnect();
+ }
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DaoSession session = dbHandler.getDaoSession();
+ Device device = DBHelper.findDevice(gbDevice, session);
+ if (device != null) {
+ deleteDevice(gbDevice, device, session);
+ QueryBuilder> qb = session.getDeviceAttributesDao().queryBuilder();
+ qb.where(DeviceAttributesDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
+ session.getDeviceDao().delete(device);
+ } else {
+ LOG.info("device to delete not found in db: " + gbDevice);
+ }
+ } catch (Exception e) {
+ throw new GBException("Error deleting device: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Hook for subclasses to perform device-specific deletion logic, e.g. db cleanup.
+ * @param gbDevice the GBDevice
+ * @param device the corresponding database Device
+ * @param session the session to use
+ * @throws GBException
+ */
+ protected abstract void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException;
+
+ @Override
public boolean allowFetchActivityData(GBDevice device) {
return device.isInitialized() && !device.isBusy() && supportsActivityDataFetching();
}
+
+ public boolean isHealthWearable(BluetoothDevice device) {
+ BluetoothClass bluetoothClass = device.getBluetoothClass();
+ if (bluetoothClass == null) {
+ LOG.warn("unable to determine bluetooth device class of " + device);
+ return false;
+ }
+ if (bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.WEARABLE
+ || bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.UNCATEGORIZED) {
+ int deviceClasses =
+ BluetoothClass.Device.HEALTH_BLOOD_PRESSURE
+ | BluetoothClass.Device.HEALTH_DATA_DISPLAY
+ | BluetoothClass.Device.HEALTH_PULSE_RATE
+ | BluetoothClass.Device.HEALTH_WEIGHING
+ | BluetoothClass.Device.HEALTH_UNCATEGORIZED
+ | BluetoothClass.Device.HEALTH_PULSE_OXIMETER
+ | BluetoothClass.Device.HEALTH_GLUCOSE;
+
+ return (bluetoothClass.getDeviceClass() & deviceClasses) != 0;
+ }
+ return false;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java
new file mode 100644
index 000000000..781548f14
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java
@@ -0,0 +1,191 @@
+package nodomain.freeyourgadget.gadgetbridge.devices;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import de.greenrobot.dao.AbstractDao;
+import de.greenrobot.dao.Property;
+import de.greenrobot.dao.query.QueryBuilder;
+import de.greenrobot.dao.query.WhereCondition;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+
+/**
+ * Base class for all sample providers. A Sample provider is device specific and provides
+ * access to the device specific samples. There are both read and write operations.
+ * @param the sample type
+ */
+public abstract class AbstractSampleProvider implements SampleProvider {
+ private static final WhereCondition[] NO_CONDITIONS = new WhereCondition[0];
+ private final DaoSession mSession;
+ private final GBDevice mDevice;
+
+ protected AbstractSampleProvider(GBDevice device, DaoSession session) {
+ mDevice = device;
+ mSession = session;
+ }
+
+ public GBDevice getDevice() {
+ return mDevice;
+ }
+
+ public DaoSession getSession() {
+ return mSession;
+ }
+
+ @Override
+ public List getAllActivitySamples(int timestamp_from, int timestamp_to) {
+ return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
+ }
+
+ @Override
+ public List getActivitySamples(int timestamp_from, int timestamp_to) {
+ if (getRawKindSampleProperty() != null) {
+ return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY);
+ } else {
+ return getActivitySamplesByActivityFilter(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY);
+ }
+ }
+
+ @Override
+ public List getSleepSamples(int timestamp_from, int timestamp_to) {
+ if (getRawKindSampleProperty() != null) {
+ return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP);
+ } else {
+ return getActivitySamplesByActivityFilter(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP);
+ }
+ }
+
+ @Override
+ public void addGBActivitySample(T activitySample) {
+ getSampleDao().insertOrReplace(activitySample);
+ }
+
+ @Override
+ public void addGBActivitySamples(T[] activitySamples) {
+ getSampleDao().insertOrReplaceInTx(activitySamples);
+ }
+
+ @Nullable
+ @Override
+ public T getLatestActivitySample() {
+ QueryBuilder qb = getSampleDao().queryBuilder();
+ Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
+ if (dbDevice == null) {
+ // no device, no sample
+ return null;
+ }
+ Property deviceProperty = getDeviceIdentifierSampleProperty();
+ qb.where(deviceProperty.eq(dbDevice.getId())).orderDesc(getTimestampSampleProperty()).limit(1);
+ List samples = qb.build().list();
+ if (samples.isEmpty()) {
+ return null;
+ }
+ T sample = samples.get(0);
+ sample.setProvider(this);
+ return sample;
+ }
+
+ protected List getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) {
+ if (getRawKindSampleProperty() == null && activityType != ActivityKind.TYPE_ALL) {
+ // if we do not have a raw kind property we cannot query anything else then TYPE_ALL
+ return Collections.emptyList();
+ }
+ QueryBuilder qb = getSampleDao().queryBuilder();
+ Property timestampProperty = getTimestampSampleProperty();
+ Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
+ if (dbDevice == null) {
+ // no device, no samples
+ return Collections.emptyList();
+ }
+ Property deviceProperty = getDeviceIdentifierSampleProperty();
+ qb.where(deviceProperty.eq(dbDevice.getId()), timestampProperty.ge(timestamp_from))
+ .where(timestampProperty.le(timestamp_to), getClauseForActivityType(qb, activityType));
+ List samples = qb.build().list();
+ for (T sample : samples) {
+ sample.setProvider(this);
+ }
+ detachFromSession();
+ return samples;
+ }
+
+ /**
+ * Detaches all samples of this type from the session. Changes to them may not be
+ * written back to the database.
+ *
+ * Subclasses should call this method after performing custom queries.
+ */
+ protected void detachFromSession() {
+ getSampleDao().detachAll();
+ }
+
+ private WhereCondition[] getClauseForActivityType(QueryBuilder qb, int activityTypes) {
+ if (activityTypes == ActivityKind.TYPE_ALL) {
+ return NO_CONDITIONS;
+ }
+
+ int[] dbActivityTypes = ActivityKind.mapToDBActivityTypes(activityTypes, this);
+ WhereCondition activityTypeCondition = getActivityTypeConditions(qb, dbActivityTypes);
+ return new WhereCondition[] { activityTypeCondition };
+ }
+
+ private WhereCondition getActivityTypeConditions(QueryBuilder qb, int[] dbActivityTypes) {
+ // What a crappy QueryBuilder API ;-( QueryBuilder.or(WhereCondition[]) with a runtime array length
+ // check would have worked just fine.
+ if (dbActivityTypes.length == 0) {
+ return null;
+ }
+ Property rawKindProperty = getRawKindSampleProperty();
+ if (rawKindProperty == null) {
+ return null;
+ }
+
+ if (dbActivityTypes.length == 1) {
+ return rawKindProperty.eq(dbActivityTypes[0]);
+ }
+ if (dbActivityTypes.length == 2) {
+ return qb.or(rawKindProperty.eq(dbActivityTypes[0]),
+ rawKindProperty.eq(dbActivityTypes[1]));
+ }
+ final int offset = 2;
+ int len = dbActivityTypes.length - offset;
+ WhereCondition[] trailingConditions = new WhereCondition[len];
+ for (int i = 0; i < len; i++) {
+ trailingConditions[i] = rawKindProperty.eq(dbActivityTypes[i + offset]);
+ }
+ return qb.or(rawKindProperty.eq(dbActivityTypes[0]),
+ rawKindProperty.eq(dbActivityTypes[1]),
+ trailingConditions);
+ }
+
+ private List getActivitySamplesByActivityFilter(int timestamp_from, int timestamp_to, int activityFilter) {
+ List samples = getAllActivitySamples(timestamp_from, timestamp_to);
+ List filteredSamples = new ArrayList<>();
+
+ for (T sample : samples) {
+ if ((sample.getKind() & activityFilter) != 0) {
+ filteredSamples.add(sample);
+ }
+ }
+ return filteredSamples;
+ }
+
+ public abstract AbstractDao getSampleDao();
+
+ @Nullable
+ protected abstract Property getRawKindSampleProperty();
+
+ @NonNull
+ protected abstract Property getTimestampSampleProperty();
+
+ @NonNull
+ protected abstract Property getDeviceIdentifierSampleProperty();
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
index 7ecc3e8d1..6f0fb39d5 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
@@ -1,11 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
+import android.annotation.TargetApi;
import android.app.Activity;
+import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import java.util.Collection;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
/**
@@ -21,7 +30,18 @@ public interface DeviceCoordinator {
String EXTRA_DEVICE_MAC_ADDRESS = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_MAC_ADDRESS";
/**
- * Checks whether this candidate handles the given candidate.
+ * Checks whether this coordinator handles the given candidate.
+ * Returns the supported device type for the given candidate or
+ * DeviceType.UNKNOWN
+ *
+ * @param candidate
+ * @return the supported device type for the given candidate.
+ */
+ @NonNull
+ DeviceType getSupportedType(GBDeviceCandidate candidate);
+
+ /**
+ * Checks whether this coordinator handles the given candidate.
*
* @param candidate
* @return true if this coordinator handles the given candidate.
@@ -36,6 +56,24 @@ public interface DeviceCoordinator {
*/
boolean supports(GBDevice device);
+ /**
+ * Returns a list of scan filters that shall be used to discover devices supported
+ * by this coordinator.
+ * @return the list of scan filters, may be empty
+ */
+ @NonNull
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ Collection extends ScanFilter> createBLEScanFilters();
+
+ GBDevice createDevice(GBDeviceCandidate candidate);
+
+ /**
+ * Deletes all information, including all related database content about the
+ * given device.
+ * @throws GBException
+ */
+ void deleteDevice(GBDevice device) throws GBException;
+
/**
* Returns the kind of device type this coordinator supports.
*
@@ -67,6 +105,14 @@ public interface DeviceCoordinator {
*/
boolean supportsActivityDataFetching();
+ /**
+ * Returns true if activity tracking is supported by the device
+ * (with this coordinator).
+ *
+ * @return
+ */
+ boolean supportsActivityTracking();
+
/**
* Returns true if activity data fetching is supported AND possible at this
* very moment. This will consider the device state (being connected/disconnected/busy...)
@@ -82,7 +128,7 @@ public interface DeviceCoordinator {
*
* @return
*/
- SampleProvider getSampleProvider();
+ SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session);
/**
* Finds an install handler for the given uri that can install the given
@@ -102,11 +148,36 @@ public interface DeviceCoordinator {
boolean supportsScreenshots();
/**
- * Returns true if this device/coordinator supports settig alarms.
+ * Returns true if this device/coordinator supports setting alarms.
*
* @return
*/
boolean supportsAlarmConfiguration();
+ /**
+ * Returns true if the given device supports heart rate measurements.
+ * @return
+ */
+ boolean supportsHeartRateMeasurement(GBDevice device);
+
int getTapString();
+
+ /**
+ * Returns the readable name of the manufacturer.
+ */
+ String getManufacturer();
+
+ /**
+ * Returns true if this device/coordinator supports managing device apps.
+ *
+ * @return
+ */
+ boolean supportsAppsManagement();
+
+ /**
+ * Returns the Activity class that will be used to manage device apps.
+ *
+ * @return
+ */
+ Class extends Activity> getAppsManagementActivity();
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java
new file mode 100644
index 000000000..1db080a2f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java
@@ -0,0 +1,163 @@
+package nodomain.freeyourgadget.gadgetbridge.devices;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.annotation.Nullable;
+import android.support.v4.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+
+/**
+ * Provides access to the list of devices managed by Gadgetbridge.
+ * Changes to the devices (e.g. connection state) or the list of devices
+ * are broadcasted via #ACTION_DEVICE_CHANGED
+ */
+public class DeviceManager {
+ private static final Logger LOG = LoggerFactory.getLogger(DeviceManager.class);
+
+ public static final String BLUETOOTH_DEVICE_ACTION_ALIAS_CHANGED = "android.bluetooth.device.action.ALIAS_CHANGED";
+ /**
+ * Intent action to notify that the list of devices has changed.
+ */
+ public static final String ACTION_DEVICES_CHANGED
+ = "nodomain.freeyourgadget.gadgetbridge.devices.devicemanager.action.devices_changed";
+ /**
+ * Intent action to notify this class that the list of devices shall be refreshed.
+ */
+ public static final String ACTION_REFRESH_DEVICELIST
+ = "nodomain.freeyourgadget.gadgetbridge.devices.devicemanager.action.set_version";
+ private final Context context;
+ /**
+ * This list is final, it will never be recreated. Only its contents change.
+ * This allows direct access to the list from ListAdapters.
+ */
+ private final List deviceList = new ArrayList<>();
+ private GBDevice selectedDevice = null;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ switch (action) {
+ case ACTION_REFRESH_DEVICELIST: // fall through
+ case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
+ refreshPairedDevices();
+ break;
+ case BluetoothDevice.ACTION_NAME_CHANGED:
+ case BLUETOOTH_DEVICE_ACTION_ALIAS_CHANGED:
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ String newName = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
+ updateDeviceName(device, newName);
+ break;
+ case GBDevice.ACTION_DEVICE_CHANGED:
+ GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ if (dev.getAddress() != null) {
+ int index = deviceList.indexOf(dev); // search by address
+ if (index >= 0) {
+ deviceList.set(index, dev);
+ } else {
+ deviceList.add(dev);
+ }
+ }
+ updateSelectedDevice(dev);
+ refreshPairedDevices();
+ break;
+ }
+ }
+ };
+
+ public DeviceManager(Context context) {
+ this.context = context;
+ IntentFilter filterLocal = new IntentFilter();
+ filterLocal.addAction(DeviceManager.ACTION_REFRESH_DEVICELIST);
+ filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
+ filterLocal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ LocalBroadcastManager.getInstance(context).registerReceiver(mReceiver, filterLocal);
+
+ IntentFilter filterGlobal = new IntentFilter();
+ filterGlobal.addAction(BluetoothDevice.ACTION_NAME_CHANGED);
+ filterGlobal.addAction(BLUETOOTH_DEVICE_ACTION_ALIAS_CHANGED);
+ filterGlobal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ context.registerReceiver(mReceiver, filterGlobal);
+
+ refreshPairedDevices();
+ }
+
+ private void updateDeviceName(BluetoothDevice device, String newName) {
+ for (GBDevice dev : deviceList) {
+ if (device.getAddress().equals(dev.getAddress())) {
+ if (!dev.getName().equals(newName)) {
+ dev.setName(newName);
+ notifyDevicesChanged();
+ return;
+ }
+ }
+ }
+ }
+
+ private void updateSelectedDevice(GBDevice dev) {
+ if (selectedDevice == null) {
+ selectedDevice = dev;
+ } else {
+ if (selectedDevice.equals(dev)) {
+ selectedDevice = dev; // equality vs identity!
+ } else {
+ if (selectedDevice.isConnected() && dev.isConnected()) {
+ LOG.warn("multiple connected devices -- this is currently not really supported");
+ selectedDevice = dev; // use the last one that changed
+ } else if (!selectedDevice.isConnected()) {
+ selectedDevice = dev; // use the last one that changed
+ }
+ }
+ }
+ }
+
+ private void refreshPairedDevices() {
+ Set availableDevices = DeviceHelper.getInstance().getAvailableDevices(context);
+ deviceList.retainAll(availableDevices);
+ for (GBDevice availableDevice : availableDevices) {
+ if (!deviceList.contains(availableDevice)) {
+ deviceList.add(availableDevice);
+ }
+ }
+
+ Collections.sort(deviceList, new Comparator() {
+ @Override
+ public int compare(GBDevice lhs, GBDevice rhs) {
+ return Collator.getInstance().compare(lhs.getName(), rhs.getName());
+ }
+ });
+ notifyDevicesChanged();
+ }
+
+ /**
+ * The returned list is final, it will never be recreated. Only its contents change.
+ * This allows direct access to the list from ListAdapters.
+ */
+ public List getDevices() {
+ return Collections.unmodifiableList(deviceList);
+ }
+
+ @Nullable
+ public GBDevice getSelectedDevice() {
+ return selectedDevice;
+ }
+
+ private void notifyDevicesChanged() {
+ LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(ACTION_DEVICES_CHANGED));
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
index a275ace51..2e6527884 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
@@ -8,12 +8,13 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
/**
- * Specifies all events that GadgetBridge intends to send to the gadget device.
+ * Specifies all events that Gadgetbridge intends to send to the gadget device.
* Implementations can decide to ignore events that they do not support.
* Implementations need to send/encode event to the connected device.
*/
@@ -26,6 +27,8 @@ public interface EventHandler {
void onSetCallState(CallSpec callSpec);
+ void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec);
+
void onSetMusicState(MusicStateSpec stateSpec);
void onSetMusicInfo(MusicSpec musicSpec);
@@ -42,6 +45,8 @@ public interface EventHandler {
void onAppConfiguration(UUID appUuid, String config);
+ void onAppReorder(UUID uuids[]);
+
void onFetchActivityData();
void onReboot();
@@ -52,6 +57,8 @@ public interface EventHandler {
void onFindDevice(boolean start);
+ void onSetConstantVibration(int integer);
+
void onScreenshotReq();
void onEnableHeartRateSleepSupport(boolean enable);
@@ -59,4 +66,13 @@ public interface EventHandler {
void onAddCalendarEvent(CalendarEventSpec calendarEventSpec);
void onDeleteCalendarEvent(byte type, long id);
+
+ /**
+ * Sets the given option in the device, typically with values from the preferences.
+ * The config name is device specific.
+ * @param config the device specific option to set on the device
+ */
+ void onSendConfiguration(String config);
+
+ void onTestNewFunction();
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java
index 78463d029..fca1710ec 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java
@@ -1,13 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
-public interface SampleProvider {
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+
+/**
+ * Interface to retrieve samples from the database, and also create and add samples to the database.
+ * There are multiple device specific implementations, this interface defines the generic access.
+ *
+ * Note that the provided samples must typically be considered read-only, because they are immediately
+ * removed from the session before they are returned.
+ *
+ * @param the device/provider specific sample type (must extend AbstractActivitySample)
+ */
+public interface SampleProvider {
+ // TODO: these constants can all be removed
int PROVIDER_MIBAND = 0;
int PROVIDER_PEBBLE_MORPHEUZ = 1;
- int PROVIDER_PEBBLE_GADGETBRIDGE = 2;
+ int PROVIDER_PEBBLE_GADGETBRIDGE = 2; // removed
int PROVIDER_PEBBLE_MISFIT = 3;
int PROVIDER_PEBBLE_HEALTH = 4;
+ int PROVIDER_MIBAND2 = 5;
int PROVIDER_UNKNOWN = 100;
+ // TODO: can also be removed
+
+ /**
+ * Returns the "id" of this sample provider, as used in Gadgetbridge versions < 0.12.0.
+ * Only used for importing old samples.
+ * @deprecated
+ */
+ int getID();
int normalizeType(int rawType);
@@ -15,5 +41,59 @@ public interface SampleProvider {
float normalizeIntensity(int rawIntensity);
- int getID();
+ /**
+ * Returns the list of all samples, of any type, within the given time span.
+ * @param timestamp_from the start timestamp
+ * @param timestamp_to the end timestamp
+ * @return the list of samples of any type
+ */
+ @NonNull
+ List getAllActivitySamples(int timestamp_from, int timestamp_to);
+
+ /**
+ * Returns the list of all samples that represent user "activity", within
+ * the given time span. This excludes samples of type sleep, for example.
+ * @param timestamp_from the start timestamp
+ * @param timestamp_to the end timestamp
+ * @return the list of samples of type user activity, e.g. non-sleep
+ */
+ @NonNull
+ List getActivitySamples(int timestamp_from, int timestamp_to);
+
+ /**
+ * Returns the list of all samples that represent "sleeping", within the
+ * given time span.
+ * @param timestamp_from the start timestamp
+ * @param timestamp_to the end timestamp
+ * @return the list of samples of type sleep
+ */
+ @NonNull
+ List getSleepSamples(int timestamp_from, int timestamp_to);
+
+ /**
+ * Adds the given sample to the database. An existing sample with the same
+ * timestamp will be overwritten.
+ * @param activitySample the sample to add
+ */
+ void addGBActivitySample(T activitySample);
+
+ /**
+ * Adds the given samples to the database. Existing samples with the same
+ * timestamp will be overwritten.
+ * @param activitySamples the samples to add
+ */
+ void addGBActivitySamples(T[] activitySamples);
+
+ /**
+ * Factory method to creates an empty sample of the correct type for this sample provider
+ * @return the newly created "empty" sample
+ */
+ T createActivitySample();
+
+ /**
+ * Returns the activity sample with the highest timestamp. or null if none
+ * @return the latest sample or null
+ */
+ @Nullable
+ T getLatestActivitySample();
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java
index 1af8da27e..6a50ba0fb 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java
@@ -3,8 +3,16 @@ package nodomain.freeyourgadget.gadgetbridge.devices;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@@ -29,6 +37,40 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return 0;
}
+ @Override
+ public List getAllActivitySamples(int timestamp_from, int timestamp_to) {
+ return null;
+ }
+
+ @Override
+ public List getActivitySamples(int timestamp_from, int timestamp_to) {
+ return null;
+ }
+
+ @Override
+ public List getSleepSamples(int timestamp_from, int timestamp_to) {
+ return null;
+ }
+
+ @Override
+ public void addGBActivitySample(AbstractActivitySample activitySample) {
+ }
+
+ @Override
+ public void addGBActivitySamples(AbstractActivitySample[] activitySamples) {
+ }
+
+ @Override
+ public AbstractActivitySample createActivitySample() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public AbstractActivitySample getLatestActivitySample() {
+ return null;
+ }
+
@Override
public int getID() {
return PROVIDER_UNKNOWN;
@@ -40,13 +82,12 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
- public boolean supports(GBDeviceCandidate candidate) {
- return false;
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ return DeviceType.UNKNOWN;
}
@Override
- public boolean supports(GBDevice device) {
- return getDeviceType().equals(device.getType());
+ protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
@@ -65,8 +106,8 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
- public SampleProvider getSampleProvider() {
- return sampleProvider;
+ public SampleProvider> getSampleProvider(GBDevice device, DaoSession session) {
+ return new UnknownSampleProvider();
}
@Override
@@ -79,6 +120,11 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
+ @Override
+ public boolean supportsActivityTracking() {
+ return false;
+ }
+
@Override
public boolean supportsScreenshots() {
return false;
@@ -89,8 +135,28 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return false;
+ }
+
@Override
public int getTapString() {
return 0;
}
+
+ @Override
+ public String getManufacturer() {
+ return "unknown";
+ }
+
+ @Override
+ public boolean supportsAppsManagement() {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewConstants.java
new file mode 100644
index 000000000..48786c241
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewConstants.java
@@ -0,0 +1,110 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.liveview;
+//Changed by Renze: Fixed brightness constants
+
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Message constants reverse-engineered by Andrew de Quincey (http://adq.livejournal.com ).
+ *
+ * @author Robert <xperimental@solidproject.de>
+ */
+public final class LiveviewConstants {
+
+ public static Charset ENCODING = StandardCharsets.ISO_8859_1;
+ public static ByteOrder BYTE_ORDER = ByteOrder.BIG_ENDIAN;
+
+ public static final byte CLOCK_24H = 0;
+ public static final byte CLOCK_12H = 1;
+
+ public static final byte MSG_GETCAPS = 1;
+ public static final byte MSG_GETCAPS_RESP = 2;
+
+ public static final byte MSG_DISPLAYTEXT = 3;
+ public static final byte MSG_DISPLAYTEXT_ACK = 4;
+
+ public static final byte MSG_DISPLAYPANEL = 5;
+ public static final byte MSG_DISPLAYPANEL_ACK = 6;
+
+ public static final byte MSG_DEVICESTATUS = 7;
+ public static final byte MSG_DEVICESTATUS_ACK = 8;
+
+ public static final byte MSG_DISPLAYBITMAP = 19;
+ public static final byte MSG_DISPLAYBITMAP_ACK = 20;
+
+ public static final byte MSG_CLEARDISPLAY = 21;
+ public static final byte MSG_CLEARDISPLAY_ACK = 22;
+
+ public static final byte MSG_SETMENUSIZE = 23;
+ public static final byte MSG_SETMENUSIZE_ACK = 24;
+
+ public static final byte MSG_GETMENUITEM = 25;
+ public static final byte MSG_GETMENUITEM_RESP = 26;
+
+ public static final byte MSG_GETALERT = 27;
+ public static final byte MSG_GETALERT_RESP = 28;
+
+ public static final byte MSG_NAVIGATION = 29;
+ public static final byte MSG_NAVIGATION_RESP = 30;
+
+ public static final byte MSG_SETSTATUSBAR = 33;
+ public static final byte MSG_SETSTATUSBAR_ACK = 34;
+
+ public static final byte MSG_GETMENUITEMS = 35;
+
+ public static final byte MSG_SETMENUSETTINGS = 36;
+ public static final byte MSG_SETMENUSETTINGS_ACK = 37;
+
+ public static final byte MSG_GETTIME = 38;
+ public static final byte MSG_GETTIME_RESP = 39;
+
+ public static final byte MSG_SETLED = 40;
+ public static final byte MSG_SETLED_ACK = 41;
+
+ public static final byte MSG_SETVIBRATE = 42;
+ public static final byte MSG_SETVIBRATE_ACK = 43;
+
+ public static final byte MSG_ACK = 44;
+
+ public static final byte MSG_SETSCREENMODE = 64;
+ public static final byte MSG_SETSCREENMODE_ACK = 65;
+
+ public static final byte MSG_GETSCREENMODE = 66;
+ public static final byte MSG_GETSCREENMODE_RESP = 67;
+
+ public static final int DEVICESTATUS_OFF = 0;
+ public static final int DEVICESTATUS_ON = 1;
+ public static final int DEVICESTATUS_MENU = 2;
+
+ public static final byte RESULT_OK = 0;
+ public static final byte RESULT_ERROR = 1;
+ public static final byte RESULT_OOM = 2;
+ public static final byte RESULT_EXIT = 3;
+ public static final byte RESULT_CANCEL = 4;
+
+ public static final int NAVACTION_PRESS = 0;
+ public static final int NAVACTION_LONGPRESS = 1;
+ public static final int NAVACTION_DOUBLEPRESS = 2;
+
+ public static final int NAVTYPE_UP = 0;
+ public static final int NAVTYPE_DOWN = 1;
+ public static final int NAVTYPE_LEFT = 2;
+ public static final int NAVTYPE_RIGHT = 3;
+ public static final int NAVTYPE_SELECT = 4;
+ public static final int NAVTYPE_MENUSELECT = 5;
+
+ public static final int ALERTACTION_CURRENT = 0;
+ public static final int ALERTACTION_FIRST = 1;
+ public static final int ALERTACTION_LAST = 2;
+ public static final int ALERTACTION_NEXT = 3;
+ public static final int ALERTACTION_PREV = 4;
+
+ public static final int BRIGHTNESS_OFF = 49;
+ public static final int BRIGHTNESS_DIM = 50;
+ public static final int BRIGHTNESS_MAX = 51;
+
+ public static final String CLIENT_SOFTWARE_VERSION = "0.0.3";
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java
new file mode 100644
index 000000000..338137cef
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java
@@ -0,0 +1,105 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.liveview;
+
+import android.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+
+public class LiveviewCoordinator extends AbstractDeviceCoordinator {
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ String name = candidate.getDevice().getName();
+ if (name != null && name.startsWith("LiveView")) {
+ return DeviceType.LIVEVIEW;
+ }
+ return DeviceType.UNKNOWN;
+ }
+
+ @Override
+ public DeviceType getDeviceType() {
+ return DeviceType.LIVEVIEW;
+ }
+
+ @Override
+ public Class extends Activity> getPairingActivity() {
+ return null;
+ }
+
+ @Override
+ public Class extends Activity> getPrimaryActivity() {
+ return null;
+ }
+
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return false;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsScreenshots() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsAlarmConfiguration() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return false;
+ }
+
+ @Override
+ public int getTapString() {
+ //TODO: changeme
+ return R.string.tap_connected_device_for_activity;
+ }
+
+ @Override
+ public String getManufacturer() {
+ return "Sony Ericsson";
+ }
+
+ @Override
+ public boolean supportsAppsManagement() {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
+
+ @Override
+ protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
+ // nothing to delete, yet
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandSampleProvider.java
new file mode 100644
index 000000000..36a87b2ee
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandSampleProvider.java
@@ -0,0 +1,57 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.miband;
+
+import android.support.annotation.NonNull;
+
+import de.greenrobot.dao.AbstractDao;
+import de.greenrobot.dao.Property;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+
+/**
+ * Base class for Mi1 and Mi2 sample providers. At the moment they both share the
+ * same activity sample class.
+ */
+public abstract class AbstractMiBandSampleProvider extends AbstractSampleProvider {
+
+ // maybe this should be configurable 256 seems way off, though.
+ private final float movementDivisor = 180.0f; //256.0f;
+
+ public AbstractMiBandSampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @Override
+ public float normalizeIntensity(int rawIntensity) {
+ return rawIntensity / movementDivisor;
+ }
+
+ @Override
+ public AbstractDao getSampleDao() {
+ return getSession().getMiBandActivitySampleDao();
+ }
+
+ @NonNull
+ @Override
+ protected Property getTimestampSampleProperty() {
+ return MiBandActivitySampleDao.Properties.Timestamp;
+ }
+
+ @NonNull
+ @Override
+ protected Property getDeviceIdentifierSampleProperty() {
+ return MiBandActivitySampleDao.Properties.DeviceId;
+ }
+
+ @Override
+ protected Property getRawKindSampleProperty() {
+ return MiBandActivitySampleDao.Properties.RawKind;
+ }
+
+ @Override
+ public MiBandActivitySample createActivitySample() {
+ return new MiBandActivitySample();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/DateTimeDisplay.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/DateTimeDisplay.java
new file mode 100644
index 000000000..40857aa45
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/DateTimeDisplay.java
@@ -0,0 +1,6 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.miband;
+
+public enum DateTimeDisplay {
+ TIME,
+ DATE_TIME
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java
new file mode 100644
index 000000000..5d40bfc18
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java
@@ -0,0 +1,107 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.miband;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelUuid;
+import android.support.annotation.NonNull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+
+public class MiBand2Coordinator extends MiBandCoordinator {
+ private static final Logger LOG = LoggerFactory.getLogger(MiBand2Coordinator.class);
+
+ @Override
+ public DeviceType getDeviceType() {
+ return DeviceType.MIBAND2;
+ }
+
+ @NonNull
+ @Override
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public Collection extends ScanFilter> createBLEScanFilters() {
+ ParcelUuid mi2Service = new ParcelUuid(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
+ ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi2Service).build();
+ return Collections.singletonList(filter);
+ }
+
+ @NonNull
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ if (candidate.supportsService(MiBand2Service.UUID_SERVICE_MIBAND2_SERVICE)) {
+ return DeviceType.MIBAND2;
+ }
+
+ // and a heuristic for now
+ try {
+ BluetoothDevice device = candidate.getDevice();
+ if (isHealthWearable(device)) {
+ String name = device.getName();
+ if (name != null && name.equalsIgnoreCase(MiBandConst.MI_BAND2_NAME)) {
+ return DeviceType.MIBAND2;
+ }
+ }
+ } catch (Exception ex) {
+ LOG.error("unable to check device support", ex);
+ }
+ return DeviceType.UNKNOWN;
+
+ }
+
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return true;
+ }
+
+ @Override
+ public boolean supportsAlarmConfiguration() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return true;
+ }
+
+ @Override
+ public SampleProvider extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return new MiBand2SampleProvider(device, session);
+ }
+
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+ return null; // not supported at the moment
+ }
+
+ public static DateTimeDisplay getDateDisplay(Context context) throws IllegalArgumentException {
+ Prefs prefs = GBApplication.getPrefs();
+ String dateFormatTime = context.getString(R.string.p_dateformat_time);
+ if (dateFormatTime.equals(prefs.getString(MiBandConst.PREF_MI2_DATEFORMAT, dateFormatTime))) {
+ return DateTimeDisplay.TIME;
+ }
+ return DateTimeDisplay.DATE_TIME;
+ }
+
+ public static boolean getActivateDisplayOnLiftWrist() {
+ Prefs prefs = GBApplication.getPrefs();
+ return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java
new file mode 100644
index 000000000..c7debdcfe
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java
@@ -0,0 +1,138 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.miband;
+
+import java.util.List;
+
+import de.greenrobot.dao.query.QueryBuilder;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+
+public class MiBand2SampleProvider extends AbstractMiBandSampleProvider {
+// these come from Mi1
+// public static final int TYPE_LIGHT_SLEEP = 5;
+// public static final int TYPE_ACTIVITY = -1;
+// public static final int TYPE_UNKNOWN = -1;
+// public static final int TYPE_NONWEAR = 3;
+// public static final int TYPE_CHARGING = 6;
+
+
+ // observed the following values so far:
+ // 00 01 02 09 0a 0b 0c 10 11
+
+ // 0 = same activity kind as before
+ // 1 = light activity walking?
+ // 3 = definitely non-wear
+ // 9 = probably light sleep, definitely some kind of sleep
+ // 10 = ignore, except for hr (if valid)
+ // 11 = probably deep sleep
+ // 12 = definitely wake up
+ // 17 = definitely not sleep related
+
+ public static final int TYPE_UNSET = -1;
+ public static final int TYPE_NO_CHANGE = 0;
+ public static final int TYPE_ACTIVITY = 1;
+ public static final int TYPE_NONWEAR = 3;
+ public static final int TYPE_CHARGING = 6;
+ public static final int TYPE_LIGHT_SLEEP = 9;
+ public static final int TYPE_DEEP_SLEEP = 11;
+ public static final int TYPE_WAKE_UP = 12;
+ // appears to be a measurement problem resulting in type = 10 and intensity = 20, at least with fw 1.0.0.39
+ public static final int TYPE_IGNORE = 10;
+
+ public MiBand2SampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @Override
+ public int getID() {
+ return SampleProvider.PROVIDER_MIBAND2;
+ }
+
+
+ @Override
+ protected List getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) {
+ List samples = super.getGBActivitySamples(timestamp_from, timestamp_to, activityType);
+ postprocess(samples);
+ return samples;
+ }
+
+ /**
+ * "Temporary" runtime post processing of activity kinds.
+ * @param samples
+ */
+ private void postprocess(List samples) {
+ if (samples.isEmpty()) {
+ return;
+ }
+
+ int lastValidKind = determinePreviousValidActivityType(samples.get(0));
+ for (MiBandActivitySample sample : samples) {
+ int rawKind = sample.getRawKind();
+ switch (rawKind) {
+ case TYPE_IGNORE:
+ case TYPE_NO_CHANGE:
+ if (lastValidKind != TYPE_UNSET) {
+ sample.setRawKind(lastValidKind);
+ }
+ break;
+ default:
+ lastValidKind = rawKind;
+ break;
+ }
+ }
+ }
+
+ private int determinePreviousValidActivityType(MiBandActivitySample sample) {
+ QueryBuilder qb = getSampleDao().queryBuilder();
+ qb.where(MiBandActivitySampleDao.Properties.DeviceId.eq(sample.getDeviceId()),
+ MiBandActivitySampleDao.Properties.UserId.eq(sample.getUserId()),
+ MiBandActivitySampleDao.Properties.Timestamp.lt(sample.getTimestamp()),
+ MiBandActivitySampleDao.Properties.RawKind.notIn(TYPE_IGNORE, TYPE_NO_CHANGE));
+ qb.limit(1);
+ List result = qb.build().list();
+ if (result.size() > 0) {
+ return result.get(0).getRawKind();
+ }
+ return TYPE_UNSET;
+ }
+
+ @Override
+ public int normalizeType(int rawType) {
+ switch (rawType) {
+ case TYPE_DEEP_SLEEP:
+ return ActivityKind.TYPE_DEEP_SLEEP;
+ case TYPE_LIGHT_SLEEP:
+ return ActivityKind.TYPE_LIGHT_SLEEP;
+ case TYPE_ACTIVITY:
+ return ActivityKind.TYPE_ACTIVITY;
+ case TYPE_NONWEAR:
+ return ActivityKind.TYPE_NOT_WORN;
+ case TYPE_CHARGING:
+ return ActivityKind.TYPE_NOT_WORN; //I believe it's a safe assumption
+ case TYPE_IGNORE:
+ default:
+ case TYPE_UNSET: // fall through
+ return ActivityKind.TYPE_UNKNOWN;
+ }
+ }
+
+ @Override
+ public int toRawActivityKind(int activityKind) {
+ switch (activityKind) {
+ case ActivityKind.TYPE_ACTIVITY:
+ return TYPE_ACTIVITY;
+ case ActivityKind.TYPE_DEEP_SLEEP:
+ return TYPE_DEEP_SLEEP;
+ case ActivityKind.TYPE_LIGHT_SLEEP:
+ return TYPE_LIGHT_SLEEP;
+ case ActivityKind.TYPE_NOT_WORN:
+ return TYPE_NONWEAR;
+ case ActivityKind.TYPE_UNKNOWN: // fall through
+ default:
+ return TYPE_UNSET;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java
new file mode 100644
index 000000000..a0b04af16
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java
@@ -0,0 +1,347 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.miband;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID;
+
+public class MiBand2Service {
+
+
+ public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0"));
+ public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1"));
+ public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D"));
+ public static final UUID UUID_SERVICE_WEIGHT_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC0 = UUID.fromString("00000000-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC1 = UUID.fromString("00000001-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC2 = UUID.fromString("00000002-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC3 = UUID.fromString("00000003-0000-3512-2118-0009af100700"); // Alarm related
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC4 = UUID.fromString("00000004-0000-3512-2118-0009af100700");
+ public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString("00000005-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC6 = UUID.fromString("00000006-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC7 = UUID.fromString("00000007-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC8 = UUID.fromString("00000008-0000-3512-2118-0009af100700");
+ // service uuid fee1
+ public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700");
+ public static final UUID UUID_UNKNOWN_CHARACTERISTIC10 = UUID.fromString("00000010-0000-3512-2118-0009af100700");
+
+ public static final int ALERT_LEVEL_NONE = 0;
+ public static final int ALERT_LEVEL_MESSAGE = 1;
+ public static final int ALERT_LEVEL_PHONE_CALL = 2;
+ public static final int ALERT_LEVEL_VIBRATE_ONLY = 3;
+
+ // set metric distance
+ // set 12 hour time mode
+
+
+// public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01"));
+//
+// public static final UUID UUID_CHARACTERISTIC_DEVICE_NAME = UUID.fromString(String.format(BASE_UUID, "FF02"));
+//
+// public static final UUID UUID_CHARACTERISTIC_NOTIFICATION = UUID.fromString(String.format(BASE_UUID, "FF03"));
+//
+// public static final UUID UUID_CHARACTERISTIC_USER_INFO = UUID.fromString(String.format(BASE_UUID, "FF04"));
+//
+// public static final UUID UUID_CHARACTERISTIC_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "FF05"));
+//
+// public static final UUID UUID_CHARACTERISTIC_REALTIME_STEPS = UUID.fromString(String.format(BASE_UUID, "FF06"));
+//
+// public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString(String.format(BASE_UUID, "FF07"));
+//
+// public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString(String.format(BASE_UUID, "FF08"));
+//
+// public static final UUID UUID_CHARACTERISTIC_LE_PARAMS = UUID.fromString(String.format(BASE_UUID, "FF09"));
+//
+// public static final UUID UUID_CHARACTERISTIC_DATE_TIME = UUID.fromString(String.format(BASE_UUID, "FF0A"));
+//
+// public static final UUID UUID_CHARACTERISTIC_STATISTICS = UUID.fromString(String.format(BASE_UUID, "FF0B"));
+//
+// public static final UUID UUID_CHARACTERISTIC_BATTERY = UUID.fromString(String.format(BASE_UUID, "FF0C"));
+//
+// public static final UUID UUID_CHARACTERISTIC_TEST = UUID.fromString(String.format(BASE_UUID, "FF0D"));
+//
+// public static final UUID UUID_CHARACTERISTIC_SENSOR_DATA = UUID.fromString(String.format(BASE_UUID, "FF0E"));
+//
+// public static final UUID UUID_CHARACTERISTIC_PAIR = UUID.fromString(String.format(BASE_UUID, "FF0F"));
+//
+// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "2A39"));
+// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT = UUID.fromString(String.format(BASE_UUID, "2A37"));
+//
+//
+//
+// /* FURTHER UUIDS that were mixed with the other params below. The base UUID for these is unknown */
+//
+// public static final byte ALIAS_LEN = 0xa;
+//
+// /*NOTIFICATIONS: usually received on the UUID_CHARACTERISTIC_NOTIFICATION characteristic */
+//
+// public static final byte NOTIFY_NORMAL = 0x0;
+//
+// public static final byte NOTIFY_FIRMWARE_UPDATE_FAILED = 0x1;
+//
+// public static final byte NOTIFY_FIRMWARE_UPDATE_SUCCESS = 0x2;
+//
+// public static final byte NOTIFY_CONN_PARAM_UPDATE_FAILED = 0x3;
+//
+// public static final byte NOTIFY_CONN_PARAM_UPDATE_SUCCESS = 0x4;
+//
+// public static final byte NOTIFY_AUTHENTICATION_SUCCESS = 0x5;
+//
+// public static final byte NOTIFY_AUTHENTICATION_FAILED = 0x6;
+//
+// public static final byte NOTIFY_FITNESS_GOAL_ACHIEVED = 0x7;
+//
+// public static final byte NOTIFY_SET_LATENCY_SUCCESS = 0x8;
+//
+// public static final byte NOTIFY_RESET_AUTHENTICATION_FAILED = 0x9;
+//
+// public static final byte NOTIFY_RESET_AUTHENTICATION_SUCCESS = 0xa;
+//
+// public static final byte NOTIFY_FW_CHECK_FAILED = 0xb;
+//
+// public static final byte NOTIFY_FW_CHECK_SUCCESS = 0xc;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_NOTIFY = 0xd;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_CALL = 0xe;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_DISCONNECT = 0xf;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_SMART_ALARM = 0x10;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_ALARM = 0x11;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_GOAL = 0x12;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_AUTH = 0x13;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_SHUTDOWN = 0x14;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_AUTH_SUCCESS = 0x15;
+//
+// public static final byte NOTIFY_STATUS_MOTOR_TEST = 0x16;
+//
+// // 0x18 is returned when we cancel data sync, perhaps is an ack for this message
+//
+// public static final byte NOTIFY_UNKNOWN = -0x1;
+//
+// public static final int NOTIFY_PAIR_CANCEL = 0xef;
+//
+// public static final int NOTIFY_DEVICE_MALFUNCTION = 0xff;
+//
+//
+// /* MESSAGES: unknown */
+//
+// public static final byte MSG_CONNECTED = 0x0;
+//
+// public static final byte MSG_DISCONNECTED = 0x1;
+//
+// public static final byte MSG_CONNECTION_FAILED = 0x2;
+//
+// public static final byte MSG_INITIALIZATION_FAILED = 0x3;
+//
+// public static final byte MSG_INITIALIZATION_SUCCESS = 0x4;
+//
+// public static final byte MSG_STEPS_CHANGED = 0x5;
+//
+// public static final byte MSG_DEVICE_STATUS_CHANGED = 0x6;
+//
+// public static final byte MSG_BATTERY_STATUS_CHANGED = 0x7;
+//
+// /* COMMANDS: usually sent to UUID_CHARACTERISTIC_CONTROL_POINT characteristic */
+//
+// public static final byte COMMAND_SET_TIMER = 0x4;
+//
+// public static final byte COMMAND_SET_FITNESS_GOAL = 0x5;
+//
+// public static final byte COMMAND_FETCH_DATA = 0x6;
+//
+// public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7;
+//
+// public static final byte COMMAND_SEND_NOTIFICATION = 0x8;
+//
+// public static final byte COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xa;
+//
+// public static final byte COMMAND_SYNC = 0xb;
+//
+// public static final byte COMMAND_REBOOT = 0xc;
+//
+// public static final byte COMMAND_SET_WEAR_LOCATION = 0xf;
+//
+// public static final byte COMMAND_STOP_SYNC_DATA = 0x11;
+//
+// public static final byte COMMAND_STOP_MOTOR_VIBRATE = 0x13;
+//
+// public static final byte COMMAND_SET_REALTIME_STEPS_NOTIFICATION = 0x3;
+//
+// public static final byte COMMAND_SET_REALTIME_STEP = 0x10;
+//
+// // Test HR
+// public static final byte COMMAND_SET_HR_SLEEP = 0x0;
+// public static final byte COMMAND_SET__HR_CONTINUOUS = 0x1;
+// public static final byte COMMAND_SET_HR_MANUAL = 0x2;
+//
+//
+// /* FURTHER COMMANDS: unchecked therefore left commented
+//
+//
+// public static final byte COMMAND_FACTORY_RESET = 0x9t;
+//
+// public static final int COMMAND_SET_COLOR_THEME = et;
+//
+// public static final byte COMMAND_GET_SENSOR_DATA = 0x12t
+//
+// */
+//
+// /* CONNECTION: unknown
+//
+// public static final CONNECTION_LATENCY_LEVEL_LOW = 0x0t;
+//
+// public static final CONNECTION_LATENCY_LEVEL_MEDIUM = 0x1t;
+//
+// public static final CONNECTION_LATENCY_LEVEL_HIGH = 0x2t;
+//
+// */
+//
+// /* MODES: probably related to the sample data structure
+// */
+//
+// public static final byte MODE_REGULAR_DATA_LEN_BYTE = 0x0;
+//
+// // was MODE_REGULAR_DATA_LEN_MINITE
+// public static final byte MODE_REGULAR_DATA_LEN_MINUTE = 0x1;
+//
+// /* PROFILE: unknown
+//
+// public static final PROFILE_STATE_UNKNOWN:I = 0x0
+//
+// public static final PROFILE_STATE_INITIALIZATION_SUCCESS:I = 0x1
+//
+// public static final PROFILE_STATE_INITIALIZATION_FAILED:I = 0x2
+//
+// public static final PROFILE_STATE_AUTHENTICATION_SUCCESS:I = 0x3
+//
+// public static final PROFILE_STATE_AUTHENTICATION_FAILED:I = 0x4
+//
+// */
+//
+// // TEST_*: sent to UUID_CHARACTERISTIC_TEST characteristic
+//
+// public static final byte TEST_DISCONNECTED_REMINDER = 0x5;
+//
+// public static final byte TEST_NOTIFICATION = 0x3;
+//
+// public static final byte TEST_REMOTE_DISCONNECT = 0x1;
+//
+// public static final byte TEST_SELFTEST = 0x2;
+
+ private static final Map MIBAND_DEBUG;
+
+ /**
+ * Mi Band 2 authentication has three steps.
+ * This is step 1: sending a "secret" key to the band.
+ * This is byte 0, followed by {@link #AUTH_BYTE} and then the key.
+ * In the response, it is byte 1 in the byte[] value.
+ */
+ public static final byte AUTH_SEND_KEY = 0x01;
+ /**
+ * Mi Band 2 authentication has three steps.
+ * This is step 2: requesting a random authentication key from the band.
+ * This is byte 0, followed by {@link #AUTH_BYTE}.
+ * In the response, it is byte 1 in the byte[] value.
+ */
+ public static final byte AUTH_REQUEST_RANDOM_AUTH_NUMBER = 0x02;
+ /**
+ * Mi Band 2 authentication has three steps.
+ * This is step 3: sending the encrypted random authentication key to the band.
+ * This is byte 0, followed by {@link #AUTH_BYTE} and then the encrypted random authentication key.
+ * In the response, it is byte 1 in the byte[] value.
+ */
+ public static final byte AUTH_SEND_ENCRYPTED_AUTH_NUMBER = 0x03;
+
+ /**
+ * Received in response to any authentication requests (byte 0 in the byte[] value.
+ */
+ public static final byte AUTH_RESPONSE = 0x10;
+ /**
+ * Received in response to any authentication requests (byte 2 in the byte[] value.
+ * 0x01 means success.
+ */
+ public static final byte AUTH_SUCCESS = 0x01;
+ /**
+ * Received in response to any authentication requests (byte 2 in the byte[] value.
+ * 0x04 means failure.
+ */
+ public static final byte AUTH_FAIL = 0x04;
+ /**
+ * In some logs it's 0x0...
+ */
+ public static final byte AUTH_BYTE = 0x8;
+
+ // maybe not really activity data, but steps?
+ public static final byte COMMAND_FETCH_ACTIVITY_DATA = 0x02;
+
+ public static final byte[] COMMAND_SET_FITNESS_GOAL_START = new byte[] { 0x10, 0x0, 0x0 };
+ public static final byte[] COMMAND_SET_FITNESS_GOAL_END = new byte[] { 0, 0 };
+
+
+ public static byte COMMAND_DATEFORMAT = 0x06;
+
+ public static final byte[] DATEFORMAT_DATE_TIME = new byte[] { COMMAND_DATEFORMAT, 0x0a, 0x0, 0x03 };
+ public static final byte[] DATEFORMAT_TIME = new byte[] { COMMAND_DATEFORMAT, 0x0a, 0x0, 0x0 };
+
+ public static final byte RESPONSE = 0x10;
+
+ public static final byte SUCCESS = 0x01;
+ public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01;
+
+ public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS };
+ /**
+ * Received in response to any dateformat configuration request (byte 0 in the byte[] value.
+ */
+ public static final byte[] RESPONSE_DATEFORMAT_SUCCESS = new byte[] { RESPONSE, COMMAND_DATEFORMAT, 0x0a, 0x0, 0x01 };
+ public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
+
+ public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
+ public static final byte[] WEAR_LOCATION_RIGHT_WRIST = new byte[] { 0x20, 0x00, 0x00, (byte) 0x82};
+
+ public static final byte[] COMMAND_ENABLE_HR_SLEEP_MEASUREMENT = new byte[]{0x15, 0x00, 0x01};
+ public static final byte[] COMMAND_DISABLE_HR_SLEEP_MEASUREMENT = new byte[]{0x15, 0x00, 0x00};
+
+ public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{0x06, 0x05, 0x00, 0x01};
+ public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{0x06, 0x05, 0x00, 0x00};
+
+
+ static {
+ MIBAND_DEBUG = new HashMap<>();
+ MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service");
+ MIBAND_DEBUG.put(UUID_SERVICE_HEART_RATE, "MiBand HR Service");
+
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_INFO, "Device Info");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_NAME, "Device Name");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_NOTIFICATION, "Notification");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_USER_INFO, "User Info");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_CONTROL_POINT, "Control Point");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_REALTIME_STEPS, "Realtime Steps");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_ACTIVITY_DATA, "Activity Data");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_FIRMWARE_DATA, "Firmware Data");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_LE_PARAMS, "LE Params");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DATE_TIME, "Date/Time");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_STATISTICS, "Statistics");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_BATTERY, "Battery");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_TEST, "Test");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_SENSOR_DATA, "Sensor Data");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_PAIR, "Pair");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT, "Heart Rate Control Point");
+// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT, "Heart Rate Measure");
+ }
+
+ public static String lookup(UUID uuid, String fallback) {
+ String name = MIBAND_DEBUG.get(uuid);
+ if (name == null) {
+ name = fallback;
+ }
+ return name;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java
index ca8b9e64e..f4d5ec9f4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java
@@ -17,13 +17,13 @@ public final class MiBandConst {
public static final String PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR = "mi_reserve_alarm_calendar";
public static final String PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION = "mi_hr_sleep_detection";
public static final String PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS = "mi_device_time_offset_hours";
+ public static final String PREF_MI2_DATEFORMAT = "mi2_dateformat";
+ public static final String PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT = "mi2_activate_display_on_lift_wrist";
- public static final String ORIGIN_SMS = "sms";
public static final String ORIGIN_INCOMING_CALL = "incoming_call";
- public static final String ORIGIN_K9MAIL = "k9mail";
- public static final String ORIGIN_PEBBLEMSG = "pebblemsg";
- public static final String ORIGIN_GENERIC = "generic";
+ public static final String MI_GENERAL_NAME_PREFIX = "MI";
+ public static final String MI_BAND2_NAME = "MI Band 2";
public static final String MI_1 = "1";
public static final String MI_1A = "1A";
public static final String MI_1S = "1S";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java
index 505d35303..abff3bc15 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java
@@ -1,18 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
+import android.annotation.TargetApi;
import android.app.Activity;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelUuid;
+import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Collection;
+import java.util.Collections;
+
+import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
@@ -21,22 +36,51 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class MiBandCoordinator extends AbstractDeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(MiBandCoordinator.class);
- private final MiBandSampleProvider sampleProvider;
public MiBandCoordinator() {
- sampleProvider = new MiBandSampleProvider();
}
+ @NonNull
@Override
- public boolean supports(GBDeviceCandidate candidate) {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public Collection extends ScanFilter> createBLEScanFilters() {
+ ParcelUuid mi1Service = new ParcelUuid(MiBandService.UUID_SERVICE_MIBAND_SERVICE);
+ ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi1Service).build();
+ return Collections.singletonList(filter);
+ }
+
+ @NonNull
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String macAddress = candidate.getMacAddress().toUpperCase();
- return macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1_1A)
- || macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1S);
+ if (macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1_1A)
+ || macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1S)) {
+ return DeviceType.MIBAND;
+ }
+ if (candidate.supportsService(MiBandService.UUID_SERVICE_MIBAND_SERVICE)
+ && !candidate.supportsService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE)) {
+ return DeviceType.MIBAND;
+ }
+ // and a heuristic
+ try {
+ BluetoothDevice device = candidate.getDevice();
+ if (isHealthWearable(device)) {
+ String name = device.getName();
+ if (name != null && name.toUpperCase().startsWith(MiBandConst.MI_GENERAL_NAME_PREFIX.toUpperCase())) {
+ return DeviceType.MIBAND;
+ }
+ }
+ } catch (Exception ex) {
+ LOG.error("unable to check device support", ex);
+ }
+ return DeviceType.UNKNOWN;
}
@Override
- public boolean supports(GBDevice device) {
- return getDeviceType().equals(device.getType());
+ protected void deleteDevice(GBDevice gbDevice, Device device, DaoSession session) throws GBException {
+ Long deviceId = device.getId();
+ QueryBuilder> qb = session.getMiBandActivitySampleDao().queryBuilder();
+ qb.where(MiBandActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
@Override
@@ -49,13 +93,14 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return MiBandPairingActivity.class;
}
+ @Override
public Class extends Activity> getPrimaryActivity() {
return ChartsActivity.class;
}
@Override
- public SampleProvider getSampleProvider() {
- return sampleProvider;
+ public SampleProvider extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return new MiBandSampleProvider(device, session);
}
@Override
@@ -79,11 +124,31 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return true;
}
+ @Override
+ public boolean supportsActivityTracking() {
+ return true;
+ }
+
@Override
public int getTapString() {
return R.string.tap_connected_device_for_activity;
}
+ @Override
+ public String getManufacturer() {
+ return "Xiaomi";
+ }
+
+ @Override
+ public boolean supportsAppsManagement() {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
+
public static boolean hasValidUserInfo() {
String dummyMacAddress = MiBandService.MAC_ADDRESS_FILTER_1_1A + ":00:00:00";
try {
@@ -122,10 +187,10 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
UserInfo info = UserInfo.create(
miBandAddress,
prefs.getString(MiBandConst.PREF_USER_ALIAS, null),
- activityUser.getActivityUserGender(),
- activityUser.getActivityUserAge(),
- activityUser.getActivityUserHeightCm(),
- activityUser.getActivityUserWeightKg(),
+ activityUser.getGender(),
+ activityUser.getAge(),
+ activityUser.getHeightCm(),
+ activityUser.getWeightKg(),
0
);
return info;
@@ -159,4 +224,18 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
Prefs prefs = GBApplication.getPrefs();
return prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
}
+
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ String hwVersion = device.getModel();
+ return isMi1S(hwVersion) || isMiPro(hwVersion);
+ }
+
+ private boolean isMi1S(String hardwareVersion) {
+ return MiBandConst.MI_1S.equals(hardwareVersion);
+ }
+
+ private boolean isMiPro(String hardwareVersion) {
+ return MiBandConst.MI_PRO.equals(hardwareVersion);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java
index 20ff39d92..04f87c717 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java
@@ -47,6 +47,9 @@ public class MiBandFWHelper {
16779782, //1.0.10.6 reported on the wiki
16779787, //1.0.10.11 tested by developer
//FW_16779790, //1.0.10.14 reported on the wiki (vibration does not work currently)
+ 68094986, // 4.15.12.10 tested by developer
+ 68158215, // 4.16.3.7 tested by developer
+ 68158486, // 4.16.4.22 tested by developer and user
84870926, // 5.15.7.14 tested by developer
};
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java
index 12d886836..89424a1ff 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java
@@ -1,6 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
-import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
@@ -21,12 +20,13 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
-public class MiBandPairingActivity extends Activity {
+public class MiBandPairingActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(MiBandPairingActivity.class);
private static final int REQ_CODE_USER_SETTINGS = 52;
@@ -43,8 +43,12 @@ public class MiBandPairingActivity extends Activity {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug("pairing activity: device changed: " + device);
- if (macAddress.equals(device.getAddress()) && device.isInitialized()) {
- pairingFinished(true);
+ if (macAddress.equals(device.getAddress())) {
+ if (device.isInitialized()) {
+ pairingFinished(true, macAddress);
+ } else if (device.isConnecting() || device.isInitializing()) {
+ LOG.info("still connecting/initializing device...");
+ }
}
}
}
@@ -55,24 +59,38 @@ public class MiBandPairingActivity extends Activity {
public void onReceive(Context context, Intent intent) {
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + bondingMacAddress);
if (bondingMacAddress != null && bondingMacAddress.equals(device.getAddress())) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
if (bondState == BluetoothDevice.BOND_BONDED) {
LOG.info("Bonded with " + device.getAddress());
bondingMacAddress = null;
- Looper mainLooper = Looper.getMainLooper();
- new Handler(mainLooper).postDelayed(new Runnable() {
- @Override
- public void run() {
- performPair();
- }
- }, DELAY_AFTER_BONDING);
+ attemptToConnect();
+ } else if (bondState == BluetoothDevice.BOND_BONDING) {
+ LOG.info("Bonding in progress with " + device.getAddress());
+ } else if (bondState == BluetoothDevice.BOND_NONE) {
+ LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway.");
+ bondingMacAddress = null;
+ attemptToConnect();
+ } else {
+ LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState);
+ pairingFinished(false, bondingMacAddress);
}
}
}
}
};
+ private void attemptToConnect() {
+ Looper mainLooper = Looper.getMainLooper();
+ new Handler(mainLooper).postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ performPair();
+ }
+ }, DELAY_AFTER_BONDING);
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -142,7 +160,7 @@ public class MiBandPairingActivity extends Activity {
private void startPairing() {
isPairing = true;
- message.setText(getString(R.string.miband_pairing, macAddress));
+ message.setText(getString(R.string.pairing, macAddress));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
@@ -157,7 +175,7 @@ public class MiBandPairingActivity extends Activity {
}
}
- private void pairingFinished(boolean pairedSuccessfully) {
+ private void pairingFinished(boolean pairedSuccessfully, String macAddress) {
LOG.debug("pairingFinished: " + pairedSuccessfully);
if (!isPairing) {
// already gone?
@@ -169,12 +187,17 @@ public class MiBandPairingActivity extends Activity {
unregisterReceiver(mBondingReceiver);
if (pairedSuccessfully) {
- Prefs prefs = GBApplication.getPrefs();
- prefs.getPreferences().edit().putString(MiBandConst.PREF_MIBAND_ADDRESS, macAddress).apply();
+ // remember the device since we do not necessarily pair... temporary -- we probably need
+ // to query the db for available devices in ControlCenter. But only remember un-bonded
+ // devices, as bonded devices are displayed anyway.
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
+ if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) {
+ Prefs prefs = GBApplication.getPrefs();
+ prefs.getPreferences().edit().putString(MiBandConst.PREF_MIBAND_ADDRESS, macAddress).apply();
+ }
+ Intent intent = new Intent(this, ControlCenter.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
}
-
- Intent intent = new Intent(this, ControlCenter.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- startActivity(intent);
finish();
}
@@ -186,24 +209,25 @@ public class MiBandPairingActivity extends Activity {
protected void performBluetoothPair(BluetoothDevice device) {
int bondState = device.getBondState();
if (bondState == BluetoothDevice.BOND_BONDED) {
- LOG.info("Already bonded: " + device.getAddress());
+ GB.toast(getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
performPair();
return;
}
bondingMacAddress = device.getAddress();
if (bondState == BluetoothDevice.BOND_BONDING) {
- GB.toast(this, "Bonding in progress: " + bondingMacAddress, Toast.LENGTH_LONG, GB.INFO);
+ GB.toast(this, getString(R.string.pairing_in_progress, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
return;
}
- GB.toast(this, "Creating bond with" + bondingMacAddress, Toast.LENGTH_LONG, GB.INFO);
+ GB.toast(this, getString(R.string.pairing_creating_bond_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
if (!device.createBond()) {
- GB.toast(this, "Unable to pair with " + bondingMacAddress, Toast.LENGTH_LONG, GB.ERROR);
+ GB.toast(this, getString(R.string.pairing_unable_to_pair_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.ERROR);
}
}
private void performPair() {
+ GBApplication.deviceService().disconnect(); // just to make sure...
GBApplication.deviceService().connect(macAddress, true);
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java
index 7fb29ce96..00b77c060 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java
@@ -4,27 +4,29 @@ import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.support.v4.content.LocalBroadcastManager;
+import android.widget.Toast;
+
+import java.util.HashSet;
+import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity;
-import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_GENERIC;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_INCOMING_CALL;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_K9MAIL;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_PEBBLEMSG;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_SMS;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ADDRESS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_DONT_ACK_TRANSFER;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_FITNESS_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_WEARSIDE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_USER_ALIAS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefKey;
public class MiBandPreferencesActivity extends AbstractSettingsActivity {
@@ -34,17 +36,7 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
addPreferencesFromResource(R.xml.miband_preferences);
- final Preference developmentMiaddr = findPreference(PREF_MIBAND_ADDRESS);
- developmentMiaddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
- @Override
- public boolean onPreferenceChange(Preference preference, Object newVal) {
- Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
- LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
- preference.setSummary(newVal.toString());
- return true;
- }
-
- });
+ addTryListeners();
final Preference enableHeartrateSleepSupport = findPreference(PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION);
enableHeartrateSleepSupport.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@@ -54,21 +46,112 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
return true;
}
});
+
+ final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT);
+ setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newVal) {
+ invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ GBApplication.deviceService().onSendConfiguration(PREF_MI2_DATEFORMAT);
+ }
+ });
+ return true;
+ }
+ });
+
+ final Preference activateDisplayOnLift = findPreference(PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT);
+ activateDisplayOnLift.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newVal) {
+ invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ GBApplication.deviceService().onSendConfiguration(PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT);
+ }
+ });
+ return true;
+ }
+ });
+
+ final Preference fitnessGoal = findPreference(PREF_MIBAND_FITNESS_GOAL);
+ fitnessGoal.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newVal) {
+ invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ GBApplication.deviceService().onSendConfiguration(PREF_MIBAND_FITNESS_GOAL);
+ }
+ });
+ return true;
+ }
+ });
+ }
+
+ /**
+ * delayed execution so that the preferences are applied first
+ */
+ private void invokeLater(Runnable runnable) {
+ getListView().post(runnable);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ final Preference developmentMiaddr = findPreference(PREF_MIBAND_ADDRESS);
+ developmentMiaddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newVal) {
+ Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
+ LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
+ preference.setSummary(newVal.toString());
+ return true;
+ }
+
+ });
+ }
+
+ private void addTryListeners() {
+ for (final NotificationType type : NotificationType.values()) {
+ String prefKey = "mi_try_" + type.getGenericType();
+ final Preference tryPref = findPreference(prefKey);
+ if (tryPref != null) {
+ tryPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ tryVibration(type);
+ return true;
+ }
+ });
+ } else {
+ GB.toast(getBaseContext(), "Unable to find preference key: " + prefKey + ", trying the vibration won't work", Toast.LENGTH_LONG, GB.WARN);
+ }
+ }
+ }
+
+ private void tryVibration(NotificationType type) {
+ NotificationSpec spec = new NotificationSpec();
+ spec.type = type;
+ GBApplication.deviceService().onNotification(spec);
}
@Override
protected String[] getPreferenceKeysWithSummary() {
- return new String[]{
- PREF_USER_ALIAS,
- PREF_MIBAND_ADDRESS,
- PREF_MIBAND_FITNESS_GOAL,
- PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR,
- PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS,
- getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_SMS),
- getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL),
- getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_K9MAIL),
- getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_PEBBLEMSG),
- getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_GENERIC),
- };
+ Set prefKeys = new HashSet<>();
+ prefKeys.add(PREF_USER_ALIAS);
+ prefKeys.add(PREF_MIBAND_ADDRESS);
+ prefKeys.add(PREF_MIBAND_FITNESS_GOAL);
+ prefKeys.add(PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR);
+ prefKeys.add(PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS);
+ prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL));
+
+ for (NotificationType type : NotificationType.values()) {
+ String key = type.getGenericType();
+ prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, key));
+ }
+
+ return prefKeys.toArray(new String[0]);
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java
index b87406e17..8f2a95c7d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java
@@ -1,11 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
-public class MiBandSampleProvider implements SampleProvider {
- public static final int TYPE_DEEP_SLEEP = 5;
- public static final int TYPE_LIGHT_SLEEP = 4;
+public class MiBandSampleProvider extends AbstractMiBandSampleProvider {
+ public static final int TYPE_DEEP_SLEEP = 4;
+ public static final int TYPE_LIGHT_SLEEP = 5;
public static final int TYPE_ACTIVITY = -1;
public static final int TYPE_UNKNOWN = -1;
public static final int TYPE_NONWEAR = 3;
@@ -19,8 +21,14 @@ public class MiBandSampleProvider implements SampleProvider {
// public static final byte TYPE_USER = 100;
// public static final byte TYPE_WALKING = 1;
- // maybe this should be configurable 256 seems way off, though.
- private final float movementDivisor = 180.0f; //256.0f;
+ public MiBandSampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @Override
+ public int getID() {
+ return SampleProvider.PROVIDER_MIBAND;
+ }
@Override
public int normalizeType(int rawType) {
@@ -57,14 +65,4 @@ public class MiBandSampleProvider implements SampleProvider {
return TYPE_UNKNOWN;
}
}
-
- @Override
- public float normalizeIntensity(int rawIntensity) {
- return rawIntensity / movementDivisor;
- }
-
- @Override
- public int getID() {
- return SampleProvider.PROVIDER_MIBAND;
- }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java
index 39d5bd69f..51d51c4e2 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java
@@ -13,8 +13,9 @@ public class MiBandService {
public static final String MAC_ADDRESS_FILTER_1S = "C8:0F:10";
public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0"));
-
+ public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1"));
public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D"));
+ public static final String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700";
public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01"));
@@ -53,8 +54,6 @@ public class MiBandService {
/* FURTHER UUIDS that were mixed with the other params below. The base UUID for these is unknown */
- public static final String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700";
-
public static final byte ALIAS_LEN = 0xa;
/*NOTIFICATIONS: usually received on the UUID_CHARACTERISTIC_NOTIFICATION characteristic */
@@ -165,6 +164,7 @@ public class MiBandService {
public static final byte COMMAND_SET__HR_CONTINUOUS = 0x1;
public static final byte COMMAND_SET_HR_MANUAL = 0x2;
+ public static final byte COMMAND_GET_SENSOR_DATA = 0x12;
/* FURTHER COMMANDS: unchecked therefore left commented
@@ -173,8 +173,6 @@ public class MiBandService {
public static final int COMMAND_SET_COLOR_THEME = et;
- public static final byte COMMAND_GET_SENSOR_DATA = 0x12t
-
*/
/* CONNECTION: unknown
@@ -209,17 +207,15 @@ public class MiBandService {
*/
- /* TEST: unkown (maybe sent to UUID_CHARACTERISTIC_TEST characteristic?
+ // TEST_*: sent to UUID_CHARACTERISTIC_TEST characteristic
- public static final TEST_DISCONNECTED_REMINDER = 0x5t
+ public static final byte TEST_DISCONNECTED_REMINDER = 0x5;
- public static final TEST_NOTIFICATION = 0x3t
+ public static final byte TEST_NOTIFICATION = 0x3;
- public static final TEST_REMOTE_DISCONNECT = 0x1t
+ public static final byte TEST_REMOTE_DISCONNECT = 0x1;
- public static final TEST_SELFTEST = 0x2t
-
- */
+ public static final byte TEST_SELFTEST = 0x2;
private static final Map MIBAND_DEBUG;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java
index 97dfc4e63..b23342bc0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java
@@ -4,6 +4,7 @@ import android.content.Context;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertLevel;
public class VibrationProfile {
public static final Context CONTEXT = GBApplication.getContext();
@@ -42,13 +43,14 @@ public class VibrationProfile {
private final int[] onOffSequence;
private final short repeat;
+ private int alertLevel = AlertLevel.MildAlert.getId();
/**
* Creates a new profile instance.
*
* @param id the ID, used as preference key.
* @param onOffSequence a sequence of alternating on and off durations, in milliseconds
- * @param repeat how ofoften the sequence shall be repeated
+ * @param repeat how often the sequence shall be repeated
*/
public VibrationProfile(String id, int[] onOffSequence, short repeat) {
this.id = id;
@@ -67,4 +69,12 @@ public class VibrationProfile {
public short getRepeat() {
return repeat;
}
+
+ public void setAlertLevel(int alertLevel) {
+ this.alertLevel = alertLevel;
+ }
+
+ public int getAlertLevel() {
+ return alertLevel;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java
deleted file mode 100644
index b62abe167..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
-
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
-
-public class HealthSampleProvider implements SampleProvider {
- public static final int TYPE_DEEP_SLEEP = 5;
- public static final int TYPE_LIGHT_SLEEP = 4;
- public static final int TYPE_ACTIVITY = -1;
-
- protected final float movementDivisor = 8000f;
-
- @Override
- public int normalizeType(int rawType) {
- switch (rawType) {
- case TYPE_DEEP_SLEEP:
- return ActivityKind.TYPE_DEEP_SLEEP;
- case TYPE_LIGHT_SLEEP:
- return ActivityKind.TYPE_LIGHT_SLEEP;
- case TYPE_ACTIVITY:
- default:
- return ActivityKind.TYPE_UNKNOWN;
- }
- }
-
- @Override
- public int toRawActivityKind(int activityKind) {
- switch (activityKind) {
- case ActivityKind.TYPE_ACTIVITY:
- return TYPE_ACTIVITY;
- case ActivityKind.TYPE_DEEP_SLEEP:
- return TYPE_DEEP_SLEEP;
- case ActivityKind.TYPE_LIGHT_SLEEP:
- return TYPE_LIGHT_SLEEP;
- case ActivityKind.TYPE_UNKNOWN: // fall through
- default:
- return TYPE_ACTIVITY;
- }
- }
-
-
- @Override
- public float normalizeIntensity(int rawIntensity) {
- return rawIntensity / movementDivisor;
- }
-
-
- @Override
- public int getID() {
- return SampleProvider.PROVIDER_PEBBLE_HEALTH;
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java
deleted file mode 100644
index bce42a375..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
-
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-
-public class MisfitSampleProvider implements SampleProvider {
-
- protected final float movementDivisor = 300f;
-
- @Override
- public int normalizeType(int rawType) {
- return (int) rawType;
- }
-
- @Override
- public int toRawActivityKind(int activityKind) {
- return (byte) activityKind;
- }
-
-
- @Override
- public float normalizeIntensity(int rawIntensity) {
- return rawIntensity / movementDivisor;
- }
-
-
- @Override
- public int getID() {
- return SampleProvider.PROVIDER_PEBBLE_MISFIT;
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java
deleted file mode 100644
index 1867c3413..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
-
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
-
-public class MorpheuzSampleProvider implements SampleProvider {
- // raw types
- public static final int TYPE_DEEP_SLEEP = 5;
- public static final int TYPE_LIGHT_SLEEP = 4;
- public static final int TYPE_ACTIVITY = -1;
- public static final int TYPE_UNKNOWN = -1;
-
- protected float movementDivisor = 5000f;
-
- @Override
- public int normalizeType(int rawType) {
- switch (rawType) {
- case TYPE_DEEP_SLEEP:
- return ActivityKind.TYPE_DEEP_SLEEP;
- case TYPE_LIGHT_SLEEP:
- return ActivityKind.TYPE_LIGHT_SLEEP;
- case TYPE_ACTIVITY:
- return ActivityKind.TYPE_ACTIVITY;
- default:
-// case TYPE_UNKNOWN: // fall through
- return ActivityKind.TYPE_UNKNOWN;
- }
- }
-
- @Override
- public int toRawActivityKind(int activityKind) {
- switch (activityKind) {
- case ActivityKind.TYPE_ACTIVITY:
- return TYPE_ACTIVITY;
- case ActivityKind.TYPE_DEEP_SLEEP:
- return TYPE_DEEP_SLEEP;
- case ActivityKind.TYPE_LIGHT_SLEEP:
- return TYPE_LIGHT_SLEEP;
- case ActivityKind.TYPE_UNKNOWN: // fall through
- default:
- return TYPE_UNKNOWN;
- }
- }
-
- @Override
- public float normalizeIntensity(int rawIntensity) {
- return rawIntensity / movementDivisor;
- }
-
- @Override
- public int getID() {
- return SampleProvider.PROVIDER_PEBBLE_MORPHEUZ;
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java
index 9d66c0dbc..70ed7f373 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java
@@ -18,6 +18,7 @@ import java.io.Writer;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
@@ -51,7 +52,7 @@ public class PBWInstallHandler implements InstallHandler {
return;
}
- String platformName = PebbleUtils.getPlatformName(device.getHardwareVersion());
+ String platformName = PebbleUtils.getPlatformName(device.getModel());
try {
mPBWReader = new PBWReader(mUri, mContext, platformName);
@@ -74,7 +75,7 @@ public class PBWInstallHandler implements InstallHandler {
installItem.setIcon(R.drawable.ic_firmware);
String hwRevision = mPBWReader.getHWRevision();
- if (hwRevision != null && hwRevision.equals(device.getHardwareVersion())) {
+ if (hwRevision != null && hwRevision.equals(device.getModel())) {
installItem.setName(mContext.getString(R.string.pbw_installhandler_pebble_firmware, ""));
installItem.setDetails(mContext.getString(R.string.pbwinstallhandler_correct_hw_revision));
@@ -135,6 +136,8 @@ public class PBWInstallHandler implements InstallHandler {
destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
destDir.mkdirs();
FileUtils.copyURItoFile(mContext, mUri, new File(destDir, app.getUUID().toString() + ".pbw"));
+
+ AppManagerActivity.addToAppOrderFile("pbwcacheorder.txt", app.getUUID());
} catch (IOException e) {
LOG.error("Installation failed: " + e.getMessage(), e);
return;
@@ -174,11 +177,12 @@ public class PBWInstallHandler implements InstallHandler {
LOG.error("Failed to open output file: " + e.getMessage(), e);
}
}
+
}
public boolean isValid() {
- // always pretend it is valid, as we cant know yet about hw/fw version
+ // always pretend it is valid, as we can't know yet about hw/fw version
return true;
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java
index 42f05b745..3e3b4710a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java
@@ -96,23 +96,38 @@ public class PBWReader {
String platformDir = "";
if (!uri.toString().endsWith(".pbz")) {
- platformDir = platform + "/";
-
/*
* for aplite and basalt it is possible to install 2.x apps which have no subfolder
* we still prefer the subfolders if present.
* chalk needs to be its subfolder
*/
- if (platform.equals("aplite") || platform.equals("basalt")) {
- boolean hasPlatformDir = false;
+ String[] platformDirs;
+ switch (platform) {
+ case "basalt":
+ platformDirs = new String[]{"basalt/"};
+ break;
+ case "chalk":
+ platformDirs = new String[]{"chalk/"};
+ break;
+ case "diorite":
+ platformDirs = new String[]{"diorite/", "aplite/"};
+ break;
+ case "emery":
+ platformDirs = new String[]{"emery/", "basalt/"};
+ break;
+ default:
+ platformDirs = new String[]{"aplite/"};
+ }
+
+ for (String dir : platformDirs) {
InputStream afin = new BufferedInputStream(cr.openInputStream(uri));
ZipInputStream zis = new ZipInputStream(afin);
ZipEntry ze;
try {
while ((ze = zis.getNextEntry()) != null) {
- if (ze.getName().startsWith(platformDir)) {
- hasPlatformDir = true;
+ if (ze.getName().startsWith(dir)) {
+ platformDir = dir;
break;
}
}
@@ -120,13 +135,13 @@ public class PBWReader {
} catch (IOException e) {
e.printStackTrace();
}
+ }
- if (!hasPlatformDir) {
- platformDir = "";
- }
+ if (platform.equals("chalk") && platformDir.equals("")) {
+ return;
}
}
-
+ LOG.info("using platformdir: '" + platformDir + "'");
String appName = null;
String appCreator = null;
String appVersion = null;
@@ -217,7 +232,7 @@ public class PBWReader {
byte[] tmp_buf = new byte[32];
ByteBuffer buf = ByteBuffer.wrap(buffer);
buf.order(ByteOrder.LITTLE_ENDIAN);
- buf.getLong(); // header, TODO: verifiy
+ buf.getLong(); // header, TODO: verify
buf.getShort(); // struct version, TODO: verify
mSdkVersion = buf.getShort();
mAppVersion = buf.getShort();
@@ -327,4 +342,4 @@ public class PBWReader {
public JSONObject getAppKeysJSON() {
return mAppKeys;
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java
index c74db0541..5953113df 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java
@@ -4,15 +4,25 @@ import android.app.Activity;
import android.content.Context;
import android.net.Uri;
+import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
-import nodomain.freeyourgadget.gadgetbridge.activities.AppManagerActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSampleDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class PebbleCoordinator extends AbstractDeviceCoordinator {
@@ -20,13 +30,12 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
}
@Override
- public boolean supports(GBDeviceCandidate candidate) {
- return candidate.getName().startsWith("Pebble");
- }
-
- @Override
- public boolean supports(GBDevice device) {
- return getDeviceType().equals(device.getType());
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ String name = candidate.getDevice().getName();
+ if (name != null && name.startsWith("Pebble")) {
+ return DeviceType.PEBBLE;
+ }
+ return DeviceType.UNKNOWN;
}
@Override
@@ -36,28 +45,40 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
@Override
public Class extends Activity> getPairingActivity() {
- return null;
+ return PebblePairingActivity.class;
}
+ @Override
public Class extends Activity> getPrimaryActivity() {
return AppManagerActivity.class;
}
@Override
- public SampleProvider getSampleProvider() {
+ protected void deleteDevice(GBDevice gbDevice, Device device, DaoSession session) throws GBException {
+ Long deviceId = device.getId();
+ QueryBuilder> qb = session.getPebbleHealthActivitySampleDao().queryBuilder();
+ qb.where(PebbleHealthActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
+ qb = session.getPebbleHealthActivityOverlayDao().queryBuilder();
+ qb.where(PebbleHealthActivityOverlayDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
+ qb = session.getPebbleMisfitSampleDao().queryBuilder();
+ qb.where(PebbleMisfitSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
+ qb = session.getPebbleMorpheuzSampleDao().queryBuilder();
+ qb.where(PebbleMorpheuzSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
+ }
+
+ @Override
+ public SampleProvider extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
Prefs prefs = GBApplication.getPrefs();
int activityTracker = prefs.getInt("pebble_activitytracker", SampleProvider.PROVIDER_PEBBLE_HEALTH);
switch (activityTracker) {
case SampleProvider.PROVIDER_PEBBLE_HEALTH:
- return new HealthSampleProvider();
+ return new PebbleHealthSampleProvider(device, session);
case SampleProvider.PROVIDER_PEBBLE_MISFIT:
- return new MisfitSampleProvider();
+ return new PebbleMisfitSampleProvider(device, session);
case SampleProvider.PROVIDER_PEBBLE_MORPHEUZ:
- return new MorpheuzSampleProvider();
- case SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE:
- return new PebbleGadgetBridgeSampleProvider();
+ return new PebbleMorpheuzSampleProvider(device, session);
default:
- return new HealthSampleProvider();
+ return new PebbleHealthSampleProvider(device, session);
}
}
@@ -72,6 +93,11 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return false;
}
+ @Override
+ public boolean supportsActivityTracking() {
+ return true;
+ }
+
@Override
public boolean supportsScreenshots() {
return true;
@@ -82,8 +108,28 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return false;
}
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return PebbleUtils.hasHRM(device.getModel());
+ }
+
@Override
public int getTapString() {
return R.string.tap_connected_device_for_app_mananger;
}
+
+ @Override
+ public String getManufacturer() {
+ return "Pebble";
+ }
+
+ @Override
+ public boolean supportsAppsManagement() {
+ return true;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return AppManagerActivity.class;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java
deleted file mode 100644
index b047fdb42..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
-
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-
-public class PebbleGadgetBridgeSampleProvider extends MorpheuzSampleProvider {
- public PebbleGadgetBridgeSampleProvider() {
- movementDivisor = 63.0f;
- }
-
- @Override
- public int getID() {
- return SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE;
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleHealthSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleHealthSampleProvider.java
new file mode 100644
index 000000000..efff6a267
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleHealthSampleProvider.java
@@ -0,0 +1,130 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
+
+import java.util.Collections;
+import java.util.List;
+
+import de.greenrobot.dao.AbstractDao;
+import de.greenrobot.dao.Property;
+import de.greenrobot.dao.query.QueryBuilder;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlay;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+
+public class PebbleHealthSampleProvider extends AbstractSampleProvider {
+ public static final int TYPE_LIGHT_SLEEP = 1;
+ public static final int TYPE_DEEP_SLEEP = 2;
+ public static final int TYPE_LIGHT_NAP = 3; //probably
+ public static final int TYPE_DEEP_NAP = 4; //probably
+ public static final int TYPE_WALK = 5; //probably
+ public static final int TYPE_ACTIVITY = -1;
+
+ protected final float movementDivisor = 8000f;
+
+ public PebbleHealthSampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @Override
+ public List getAllActivitySamples(int timestamp_from, int timestamp_to) {
+ List samples = super.getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
+
+ Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
+ if (dbDevice == null) {
+ // no device, no samples
+ return Collections.emptyList();
+ }
+
+ QueryBuilder qb = getSession().getPebbleHealthActivityOverlayDao().queryBuilder();
+
+ // I assume it returns the records by id ascending ... (last overlay is dominant)
+ qb.where(PebbleHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()), PebbleHealthActivityOverlayDao.Properties.TimestampFrom.ge(timestamp_from))
+ .where(PebbleHealthActivityOverlayDao.Properties.TimestampTo.le(timestamp_to));
+ List overlayRecords = qb.build().list();
+
+ for (PebbleHealthActivityOverlay overlay : overlayRecords) {
+ for (PebbleHealthActivitySample sample : samples) {
+ if (overlay.getTimestampFrom() <= sample.getTimestamp() && sample.getTimestamp() < overlay.getTimestampTo()) {
+ // patch in the raw kind
+ sample.setRawKind(overlay.getRawKind());
+ }
+ }
+ }
+ detachFromSession();
+ return samples;
+ }
+
+ @Override
+ public AbstractDao getSampleDao() {
+ return getSession().getPebbleHealthActivitySampleDao();
+ }
+
+ @Override
+ protected Property getTimestampSampleProperty() {
+ return PebbleHealthActivitySampleDao.Properties.Timestamp;
+ }
+
+ @Override
+ protected Property getRawKindSampleProperty() {
+ return null;
+ // it is still in the database just hide it for now. remove these two commented lines later
+ //return PebbleHealthActivitySampleDao.Properties.RawKind;
+ }
+
+ @Override
+ protected Property getDeviceIdentifierSampleProperty() {
+ return PebbleHealthActivitySampleDao.Properties.DeviceId;
+ }
+
+ @Override
+ public PebbleHealthActivitySample createActivitySample() {
+ return new PebbleHealthActivitySample();
+ }
+
+ @Override
+ public int normalizeType(int rawType) {
+ switch (rawType) {
+ case TYPE_DEEP_NAP:
+ case TYPE_DEEP_SLEEP:
+ return ActivityKind.TYPE_DEEP_SLEEP;
+ case TYPE_LIGHT_NAP:
+ case TYPE_LIGHT_SLEEP:
+ return ActivityKind.TYPE_LIGHT_SLEEP;
+ case TYPE_ACTIVITY:
+ return ActivityKind.TYPE_ACTIVITY;
+ default:
+ return ActivityKind.TYPE_UNKNOWN;
+ }
+ }
+
+ @Override
+ public int toRawActivityKind(int activityKind) {
+ switch (activityKind) {
+ case ActivityKind.TYPE_ACTIVITY:
+ return TYPE_ACTIVITY;
+ case ActivityKind.TYPE_DEEP_SLEEP:
+ return TYPE_DEEP_SLEEP;
+ case ActivityKind.TYPE_LIGHT_SLEEP:
+ return TYPE_LIGHT_SLEEP;
+ default:
+ return TYPE_ACTIVITY;
+ }
+ }
+
+ @Override
+ public float normalizeIntensity(int rawIntensity) {
+ return rawIntensity / movementDivisor;
+ }
+
+ @Override
+ public int getID() {
+ return SampleProvider.PROVIDER_PEBBLE_HEALTH;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java
index 18b409ae0..89069a8c3 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java
@@ -85,11 +85,25 @@ public final class PebbleIconID {
public static final int SETTINGS = 83;
public static final int SUNRISE = 84;
public static final int SUNSET = 85;
- public static final int FACETIME_DISMISSED = 86;
- public static final int FACETIME_INCOMING = 87;
- public static final int FACETIME_OUTGOING = 88;
- public static final int FACETIME_MISSED = 89;
- public static final int FACETIME_DURING = 90;
- public static final int BLUESCREEN_OF_DEATH = 91;
- public static final int START_MUSIC_PHONE = 92;
+ public static final int RESULT_UNMUTE = 86;
+ public static final int RESULT_UNMUTE_ALT = 94;
+ public static final int DURING_PHONE_CALL_CENTERED = 95;
+ public static final int TIMELINE_EMPTY_CALENDAR = 96;
+ public static final int THUMBS_UP = 97;
+ public static final int ARROW_UP = 98;
+ public static final int ARROW_DOWN = 99;
+ public static final int ACTIVITY = 100;
+ public static final int SLEEP = 101;
+ public static final int REWARD_BAD = 102;
+ public static final int REWARD_GOOD = 103;
+ public static final int REWARD_AVERAGE = 104;
+ public static final int NOTIFICATION_FACETIME = 110;
+
+ // 4.x only from here
+ public static final int NOTIFICATION_AMAZON = 111;
+ public static final int NOTIFICATION_GOOGLE_MAPS = 112;
+ public static final int NOTIFICATION_GOOGLE_PHOTOS = 113;
+ public static final int NOTIFICATION_IOS_PHOTOS = 114;
+ public static final int NOTIFICATION_LINKEDIN = 115;
+ public static final int NOTIFICATION_SLACK = 116;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMisfitSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMisfitSampleProvider.java
new file mode 100644
index 000000000..98bc244cd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMisfitSampleProvider.java
@@ -0,0 +1,62 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
+
+import de.greenrobot.dao.AbstractDao;
+import de.greenrobot.dao.Property;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSample;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+
+public class PebbleMisfitSampleProvider extends AbstractSampleProvider {
+
+ protected final float movementDivisor = 300f;
+
+ public PebbleMisfitSampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @Override
+ public int normalizeType(int rawType) {
+ return rawType;
+ }
+
+ @Override
+ public int toRawActivityKind(int activityKind) {
+ return activityKind;
+ }
+
+ @Override
+ public float normalizeIntensity(int rawIntensity) {
+ return rawIntensity / movementDivisor;
+ }
+
+ @Override
+ public PebbleMisfitSample createActivitySample() {
+ return new PebbleMisfitSample();
+ }
+
+ @Override
+ public int getID() {
+ return SampleProvider.PROVIDER_PEBBLE_MISFIT;
+ }
+
+ @Override
+ public AbstractDao getSampleDao() {
+ return getSession().getPebbleMisfitSampleDao();
+ }
+
+ @Override
+ protected Property getRawKindSampleProperty() {
+ return null;
+ }
+
+ protected Property getTimestampSampleProperty() {
+ return PebbleMisfitSampleDao.Properties.Timestamp;
+ }
+
+ protected Property getDeviceIdentifierSampleProperty() {
+ return PebbleMisfitSampleDao.Properties.DeviceId;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMorpheuzSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMorpheuzSampleProvider.java
new file mode 100644
index 000000000..8bdf8f017
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMorpheuzSampleProvider.java
@@ -0,0 +1,65 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
+
+import de.greenrobot.dao.AbstractDao;
+import de.greenrobot.dao.Property;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSample;
+import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+
+public class PebbleMorpheuzSampleProvider extends AbstractSampleProvider {
+
+ protected float movementDivisor = 5000f;
+
+ public PebbleMorpheuzSampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @Override
+ public AbstractDao getSampleDao() {
+ return getSession().getPebbleMorpheuzSampleDao();
+ }
+
+ @Override
+ protected Property getTimestampSampleProperty() {
+ return PebbleMorpheuzSampleDao.Properties.Timestamp;
+ }
+
+ @Override
+ protected Property getRawKindSampleProperty() {
+ return null; // not supported
+ }
+
+ @Override
+ protected Property getDeviceIdentifierSampleProperty() {
+ return PebbleMorpheuzSampleDao.Properties.DeviceId;
+ }
+
+ @Override
+ public PebbleMorpheuzSample createActivitySample() {
+ return new PebbleMorpheuzSample();
+ }
+
+
+ @Override
+ public float normalizeIntensity(int rawIntensity) {
+ return rawIntensity / movementDivisor;
+ }
+
+ @Override
+ public int getID() {
+ return SampleProvider.PROVIDER_PEBBLE_MORPHEUZ;
+ }
+
+ @Override
+ public int normalizeType(int rawType) {
+ return rawType;
+ }
+
+ @Override
+ public int toRawActivityKind(int activityKind) {
+ return activityKind;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java
new file mode 100644
index 000000000..2cf3b545c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java
@@ -0,0 +1,242 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
+
+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 android.support.v4.content.LocalBroadcastManager;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import de.greenrobot.dao.query.Query;
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
+import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class PebblePairingActivity extends GBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(PebblePairingActivity.class);
+ private TextView message;
+ private boolean isPairing;
+ private boolean isLEPebble;
+ private String macAddress;
+ private BluetoothDevice mBtDevice;
+
+ private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
+ GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ LOG.debug("pairing activity: device changed: " + device);
+ if (macAddress.equals(device.getAddress()) || macAddress.equals(device.getVolatileAddress())) {
+ if (device.isInitialized()) {
+ pairingFinished(true);
+ } else if (device.isConnecting() || device.isInitializing()) {
+ LOG.info("still connecting/initializing device...");
+ }
+ }
+ }
+ }
+ };
+
+ private final BroadcastReceiver mBondingReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + macAddress);
+ if (macAddress != null && macAddress.equals(device.getAddress())) {
+ int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
+ if (bondState == BluetoothDevice.BOND_BONDED) {
+ LOG.info("Bonded with " + device.getAddress());
+ if (!isLEPebble) {
+ performConnect(null);
+ }
+ } else if (bondState == BluetoothDevice.BOND_BONDING) {
+ LOG.info("Bonding in progress with " + device.getAddress());
+ } else if (bondState == BluetoothDevice.BOND_NONE) {
+ LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway.");
+ } else {
+ LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState);
+ pairingFinished(false);
+ }
+ }
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_pebble_pairing);
+
+ message = (TextView) findViewById(R.id.pebble_pair_message);
+ Intent intent = getIntent();
+ macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
+ if (macAddress == null) {
+ Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
+ returnToPairingActivity();
+ return;
+ }
+
+ mBtDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
+ if (mBtDevice == null) {
+ GB.toast(this, "No such Bluetooth Device: " + macAddress, Toast.LENGTH_LONG, GB.ERROR);
+ returnToPairingActivity();
+ return;
+ }
+
+ isLEPebble = mBtDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE;
+
+ GBDevice gbDevice = null;
+ if (isLEPebble) {
+ if (mBtDevice.getName().startsWith("Pebble-LE ") || mBtDevice.getName().startsWith("Pebble Time LE ")) {
+ if (!GBApplication.getPrefs().getBoolean("pebble_force_le", false)) {
+ GB.toast(this, "Please switch on \"Always prefer BLE\" option in Pebble settings before pairing you Pebble LE", Toast.LENGTH_LONG, GB.ERROR);
+ returnToPairingActivity();
+ return;
+ }
+ gbDevice = getMatchingParentDeviceFromDB(mBtDevice);
+ if (gbDevice == null) {
+ return;
+ }
+ }
+ }
+ startPairing(gbDevice);
+ }
+
+ @Override
+ protected void onDestroy() {
+ try {
+ // just to be sure, remove the receivers -- might actually be already unregistered
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
+ unregisterReceiver(mBondingReceiver);
+ } catch (IllegalArgumentException ex) {
+ // already unregistered, ignore
+ }
+ if (isPairing) {
+ stopPairing();
+ }
+
+ super.onDestroy();
+ }
+
+ private void startPairing(GBDevice gbDevice) {
+ isPairing = true;
+ message.setText(getString(R.string.pairing, macAddress));
+
+ IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
+ LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
+ filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ registerReceiver(mBondingReceiver, filter);
+
+ performPair(gbDevice);
+ }
+
+ private void pairingFinished(boolean pairedSuccessfully) {
+ LOG.debug("pairingFinished: " + pairedSuccessfully);
+ if (!isPairing) {
+ // already gone?
+ return;
+ }
+
+ isPairing = false;
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
+ unregisterReceiver(mBondingReceiver);
+
+ if (pairedSuccessfully) {
+ Intent intent = new Intent(this, ControlCenter.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+ finish();
+ }
+
+ private void stopPairing() {
+ // TODO
+ isPairing = false;
+ }
+
+ protected void performPair(GBDevice gbDevice) {
+ int bondState = mBtDevice.getBondState();
+ if (bondState == BluetoothDevice.BOND_BONDED) {
+ GB.toast(getString(R.string.pairing_already_bonded, mBtDevice.getName(), mBtDevice.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
+ return;
+ }
+
+ if (bondState == BluetoothDevice.BOND_BONDING) {
+ GB.toast(this, getString(R.string.pairing_in_progress, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO);
+ return;
+ }
+
+ GB.toast(this, getString(R.string.pairing_creating_bond_with, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO);
+ GBApplication.deviceService().disconnect(); // just to make sure...
+
+ if (isLEPebble) {
+ performConnect(gbDevice);
+ } else {
+ mBtDevice.createBond();
+ }
+ }
+
+ private void performConnect(GBDevice gbDevice) {
+ if (gbDevice == null) {
+ gbDevice = new GBDevice(mBtDevice.getAddress(), mBtDevice.getName(), DeviceType.PEBBLE);
+ }
+ GBApplication.deviceService().connect(gbDevice);
+ }
+
+ private void returnToPairingActivity() {
+ startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
+ finish();
+ }
+
+ private GBDevice getMatchingParentDeviceFromDB(BluetoothDevice btDevice) {
+ String expectedSuffix = btDevice.getName();
+ expectedSuffix = expectedSuffix.replace("Pebble-LE ", "");
+ expectedSuffix = expectedSuffix.replace("Pebble Time LE ", "");
+ expectedSuffix = expectedSuffix.substring(0, 2) + ":" + expectedSuffix.substring(2);
+ LOG.info("will try to find a Pebble with BT address suffix " + expectedSuffix);
+ GBDevice gbDevice = null;
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DaoSession session = dbHandler.getDaoSession();
+ DeviceDao deviceDao = session.getDeviceDao();
+ Query query = deviceDao.queryBuilder().where(DeviceDao.Properties.Type.eq(1), DeviceDao.Properties.Identifier.like("%" + expectedSuffix)).build();
+ List devices = query.list();
+ if (devices.size() == 0) {
+ GB.toast("Please pair your non-LE Pebble before pairing the LE one", Toast.LENGTH_SHORT, GB.INFO);
+ returnToPairingActivity();
+ return null;
+ } else if (devices.size() > 1) {
+ GB.toast("Can not match this Pebble LE to a unique device", Toast.LENGTH_SHORT, GB.INFO);
+ returnToPairingActivity();
+ return null;
+ }
+ DeviceHelper deviceHelper = DeviceHelper.getInstance();
+ gbDevice = deviceHelper.toGBDevice(devices.get(0));
+ gbDevice.setVolatileAddress(btDevice.getAddress());
+ } catch (Exception e) {
+ GB.toast("Error retrieving devices from database", Toast.LENGTH_SHORT, GB.ERROR);
+ returnToPairingActivity();
+ return null;
+ }
+ return gbDevice;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java
new file mode 100644
index 000000000..8f71f2340
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java
@@ -0,0 +1,105 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo;
+
+import android.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+
+public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ String name = candidate.getDevice().getName();
+ if (name != null && name.startsWith("Vibratissimo")) {
+ return DeviceType.VIBRATISSIMO;
+ }
+ return DeviceType.UNKNOWN;
+ }
+
+ @Override
+ public DeviceType getDeviceType() {
+ return DeviceType.VIBRATISSIMO;
+ }
+
+ @Override
+ public Class extends Activity> getPairingActivity() {
+ return null;
+ }
+
+ @Override
+ public Class extends Activity> getPrimaryActivity() {
+ return VibrationActivity.class;
+ }
+
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return false;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsScreenshots() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsAlarmConfiguration() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return false;
+ }
+
+ @Override
+ public int getTapString() {
+ return R.string.tap_connected_device_for_vibration;
+ }
+
+ @Override
+ public String getManufacturer() {
+ return "Amor AG";
+ }
+
+ @Override
+ public boolean supportsAppsManagement() {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
+
+ @Override
+ protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
+ // nothing to delete, yet
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/.gitignore b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/.gitignore
new file mode 100644
index 000000000..4d5306dbf
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/.gitignore
@@ -0,0 +1 @@
+*.java
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java
new file mode 100644
index 000000000..03f9e4421
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java
@@ -0,0 +1,89 @@
+package nodomain.freeyourgadget.gadgetbridge.entities;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+
+public abstract class AbstractActivitySample implements ActivitySample {
+ private SampleProvider mProvider;
+
+ @Override
+ public SampleProvider getProvider() {
+ return mProvider;
+ }
+
+ public void setProvider(SampleProvider provider) {
+ mProvider = provider;
+ }
+
+ @Override
+ public int getKind() {
+ return getProvider().normalizeType(getRawKind());
+ }
+
+ @Override
+ public int getRawKind() {
+ return NOT_MEASURED;
+ }
+
+ @Override
+ public float getIntensity() {
+ return getProvider().normalizeIntensity(getRawIntensity());
+ }
+
+ public void setRawKind(int kind) {
+ }
+
+ public void setRawIntensity(int intensity) {
+ }
+
+ public void setSteps(int steps) {
+ }
+
+ /**
+ * Unix timestamp of the sample, i.e. the number of seconds since 1970-01-01 00:00:00 UTC.
+ */
+ public abstract void setTimestamp(int timestamp);
+
+ public abstract void setUserId(long userId);
+
+ @Override
+ public void setHeartRate(int heartRate) {
+ }
+
+ @Override
+ public int getHeartRate() {
+ return NOT_MEASURED;
+ }
+
+ public abstract void setDeviceId(long deviceId);
+
+ public abstract long getDeviceId();
+
+ public abstract long getUserId();
+
+ @Override
+ public int getRawIntensity() {
+ return NOT_MEASURED;
+ }
+
+ @Override
+ public int getSteps() {
+ return NOT_MEASURED;
+ }
+
+ @Override
+ public String toString() {
+ int kind = getProvider() != null ? getKind() : ActivitySample.NOT_MEASURED;
+ float intensity = getProvider() != null ? getIntensity() : ActivitySample.NOT_MEASURED;
+ return getClass().getSimpleName() + "{" +
+ "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimeStamp(getTimestamp())) +
+ ", intensity=" + intensity +
+ ", steps=" + getSteps() +
+ ", heartrate=" + getHeartRate() +
+ ", type=" + kind +
+ ", userId=" + getUserId() +
+ ", deviceId=" + getDeviceId() +
+ '}';
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleHealthActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleHealthActivitySample.java
new file mode 100644
index 000000000..79f277b12
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleHealthActivitySample.java
@@ -0,0 +1,19 @@
+package nodomain.freeyourgadget.gadgetbridge.entities;
+
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+
+public abstract class AbstractPebbleHealthActivitySample extends AbstractActivitySample {
+ abstract public byte[] getRawPebbleHealthData();
+
+ private transient int rawActivityKind = ActivityKind.TYPE_UNKNOWN;
+
+ @Override
+ public int getRawKind() {
+ return rawActivityKind;
+ }
+
+ @Override
+ public void setRawKind(int kind) {
+ this.rawActivityKind = kind;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMisfitActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMisfitActivitySample.java
new file mode 100644
index 000000000..53e47c1a4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMisfitActivitySample.java
@@ -0,0 +1,54 @@
+package nodomain.freeyourgadget.gadgetbridge.entities;
+
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+
+public abstract class AbstractPebbleMisfitActivitySample extends AbstractActivitySample {
+ abstract public int getRawPebbleMisfitSample();
+
+ private transient int intensity = 0;
+ private transient int steps = 0;
+ private transient int activityKind = ActivityKind.TYPE_UNKNOWN;
+
+ private void calculate() {
+ int sample = getRawPebbleMisfitSample();
+
+ if (((sample & 0x83ff) == 0x0001) && ((sample & 0xff00) <= 0x4800)) {
+ // sleep seems to be from 0x2401 to 0x4801 (0b0IIIII0000000001) where I = intensity ?
+ intensity = (sample & 0x7c00) >>> 10;
+ // 9-18 decimal after shift
+ if (intensity <= 13) {
+ activityKind = ActivityKind.TYPE_DEEP_SLEEP;
+ } else {
+ // FIXME: this leads to too much false positives, ignore for now
+ //activityKind = ActivityKind.TYPE_LIGHT_SLEEP;
+ //intensity *= 2; // better visual distinction
+ }
+ } else {
+ if ((sample & 0x0001) == 0) { // 16-??? steps encoded in bits 1-7
+ steps = (sample & 0x00fe);
+ } else { // 0-14 steps encoded in bits 1-3, most of the time fc71 bits are set in that case
+ steps = (sample & 0x000e);
+ }
+ intensity = steps;
+ activityKind = ActivityKind.TYPE_ACTIVITY;
+ }
+ }
+
+ @Override
+ public int getSteps() {
+ calculate();
+ return steps;
+ }
+
+ @Override
+ public int getKind() {
+ calculate();
+ return activityKind;
+ }
+
+ @Override
+ public int getRawIntensity() {
+ calculate();
+ return intensity;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMorpheuzActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMorpheuzActivitySample.java
new file mode 100644
index 000000000..4a47d7b11
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMorpheuzActivitySample.java
@@ -0,0 +1,17 @@
+package nodomain.freeyourgadget.gadgetbridge.entities;
+
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+
+public abstract class AbstractPebbleMorpheuzActivitySample extends AbstractActivitySample {
+
+ @Override
+ public int getKind() {
+ int rawIntensity = getRawIntensity();
+ if (rawIntensity <= 120) {
+ return ActivityKind.TYPE_DEEP_SLEEP;
+ } else if (rawIntensity <= 1000) {
+ return ActivityKind.TYPE_LIGHT_SLEEP;
+ }
+ return ActivityKind.TYPE_ACTIVITY;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java
index e9470d31a..9c4a581ac 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java
@@ -1,11 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
+import android.Manifest;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationManager;
+import android.support.v4.app.ActivityCompat;
import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA;
@@ -64,6 +70,21 @@ public class AlarmReceiver extends BroadcastReceiver {
float latitude = prefs.getFloat("location_latitude", 0);
float longitude = prefs.getFloat("location_longitude", 0);
LOG.info("got longitude/latitude from preferences: " + latitude + "/" + longitude);
+
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
+ prefs.getBoolean("use_updated_location_if_available", false)) {
+ LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ Criteria criteria = new Criteria();
+ String provider = locationManager.getBestProvider(criteria, false);
+ if (provider != null) {
+ Location lastKnownLocation = locationManager.getLastKnownLocation(provider);
+ if (lastKnownLocation != null) {
+ latitude = (float) lastKnownLocation.getLatitude();
+ longitude = (float) lastKnownLocation.getLongitude();
+ LOG.info("got longitude/latitude from last known location: " + latitude + "/" + longitude);
+ }
+ }
+ }
GregorianCalendar[] sunriseTransitSetTomorrow = SPA.calculateSunriseTransitSet(dateTimeTomorrow, latitude, longitude, DeltaT.estimate(dateTimeTomorrow));
CalendarEventSpec calendarEventSpec = new CalendarEventSpec();
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java
index 73d26d0d4..0e639733f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java
@@ -7,7 +7,7 @@ import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
-import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class BluetoothStateChangeReceiver extends BroadcastReceiver {
@@ -18,7 +18,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_ON) {
- Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
+ Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent);
Prefs prefs = GBApplication.getPrefs();
@@ -28,11 +28,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
GBApplication.deviceService().connect();
} else if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_OFF) {
- GBApplication.deviceService().quit();
-
- Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
-
- LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
+ GBApplication.quit();
}
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java
index 439c34b09..c9431b43b 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java
@@ -54,10 +54,10 @@ public class K9Receiver extends BroadcastReceiver {
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = -1;
- notificationSpec.type = NotificationType.EMAIL;
+ notificationSpec.type = NotificationType.GENERIC_EMAIL;
/*
- * there seems to be no way to specify the the uri in the where clause.
+ * there seems to be no way to specify the uri in the where clause.
* If we do so, we just get the newest message, not the one requested.
* So, we will just search our message and match the uri manually.
* It should be the first one returned by the query in most cases,
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java
index 67c7e8e70..3edfb8815 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java
@@ -3,30 +3,54 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.os.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
public class MusicPlaybackReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(MusicPlaybackReceiver.class);
+ private static MusicSpec lastMusicSpec = new MusicSpec();
+ private static MusicStateSpec lastStatecSpec = new MusicStateSpec();
@Override
public void onReceive(Context context, Intent intent) {
- String artist = intent.getStringExtra("artist");
- String album = intent.getStringExtra("album");
- String track = intent.getStringExtra("track");
-
- LOG.info("Current track: " + artist + ", " + album + ", " + track);
-
+ /*
+ Bundle bundle = intent.getExtras();
+ for (String key : bundle.keySet()) {
+ Object value = bundle.get(key);
+ LOG.info(String.format("%s %s (%s)", key,
+ value != null ? value.toString() : "null", value != null ? value.getClass().getName() : "no class"));
+ }
+ */
MusicSpec musicSpec = new MusicSpec();
- musicSpec.artist = artist;
- musicSpec.album = album;
- musicSpec.track = track;
+ musicSpec.artist = intent.getStringExtra("artist");
+ musicSpec.album = intent.getStringExtra("album");
+ musicSpec.track = intent.getStringExtra("track");
+ musicSpec.duration = intent.getIntExtra("duration", 0) / 1000;
- GBApplication.deviceService().onSetMusicInfo(musicSpec);
+ if (!lastMusicSpec.equals(musicSpec)) {
+ lastMusicSpec = musicSpec;
+ LOG.info("Update Music Info: " + musicSpec.artist + " / " + musicSpec.album + " / " + musicSpec.track);
+ GBApplication.deviceService().onSetMusicInfo(musicSpec);
+ } else {
+ LOG.info("got metadata changed intent, but nothing changed, ignoring.");
+ }
+
+ if (intent.hasExtra("position") && intent.hasExtra("playing")) {
+ MusicStateSpec stateSpec = new MusicStateSpec();
+ stateSpec.position = intent.getIntExtra("position", 0) / 1000;
+ stateSpec.state = (byte) (intent.getBooleanExtra("playing", true) ? MusicStateSpec.STATE_PLAYING : MusicStateSpec.STATE_PAUSED);
+ if (!lastStatecSpec.equals(stateSpec)) {
+ LOG.info("Update Music State: state=" + stateSpec.state + ", position= " + stateSpec.position);
+ GBApplication.deviceService().onSetMusicState(stateSpec);
+ } else {
+ LOG.info("got state changed intent, but not enough has changed, ignoring.");
+ }
+ lastStatecSpec = stateSpec;
+ }
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java
index d8e0f0214..ca1ef0e19 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java
@@ -18,7 +18,6 @@ import android.media.session.PlaybackState;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
-import android.provider.MediaStore;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.v4.app.NotificationCompat;
@@ -31,6 +30,7 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -194,6 +194,11 @@ public class NotificationListener extends NotificationListenerService {
}
}
+ //don't forward group summary notifications to the wearable, they are meant for the android device only
+ if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) {
+ return;
+ }
+
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
return;
}
@@ -210,12 +215,6 @@ public class NotificationListener extends NotificationListenerService {
return;
}
- if (source.equals("eu.siacs.conversations")) {
- if (!"never".equals(prefs.getString("notification_mode_pebblemsg", "when_screen_off"))) {
- return;
- }
- }
-
if (source.equals("com.fsck.k9")) {
if (!"never".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) {
return;
@@ -250,43 +249,22 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.sourceName = (String) pm.getApplicationLabel(ai);
}
- switch (source) {
- case "org.mariotaku.twidere":
- case "com.twitter.android":
- case "org.andstatus.app":
- case "org.mustard.android":
- notificationSpec.type = NotificationType.TWITTER;
- break;
- case "com.fsck.k9":
- case "com.android.email":
- notificationSpec.type = NotificationType.EMAIL;
- break;
- case "com.moez.QKSMS":
- case "com.android.mms":
- case "com.android.messaging":
- case "com.sonyericsson.conversations":
- case "org.smssecure.smssecure":
- notificationSpec.type = NotificationType.SMS;
- break;
- case "eu.siacs.conversations":
- case "org.thoughtcrime.securesms":
- notificationSpec.type = NotificationType.CHAT;
- break;
- case "org.indywidualni.fblite":
- notificationSpec.type = NotificationType.FACEBOOK;
- break;
- default:
- notificationSpec.type = NotificationType.UNDEFINED;
- break;
- }
+ boolean preferBigText = false;
+
+ notificationSpec.type = AppNotificationType.getInstance().get(source);
LOG.info("Processing notification from source " + source);
- dissectNotificationTo(notification, notificationSpec);
+ dissectNotificationTo(notification, notificationSpec, preferBigText);
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification);
List actions = wearableExtender.getActions();
+
+ if (actions.isEmpty() && notificationSpec.type == NotificationType.TELEGRAM) {
+ return; // workaround for duplicate telegram message
+ }
+
for (NotificationCompat.Action act : actions) {
if (act != null && act.getRemoteInputs() != null) {
LOG.info("found wearable action: " + act.getTitle() + " " + sbn.getTag());
@@ -299,18 +277,26 @@ public class NotificationListener extends NotificationListenerService {
GBApplication.deviceService().onNotification(notificationSpec);
}
- private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec) {
+ private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec, boolean preferBigText) {
Bundle extras = notification.extras;
+
+ //dumpExtras(extras);
+
CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE);
if (title != null) {
notificationSpec.title = title.toString();
}
- if (extras.containsKey(Notification.EXTRA_TEXT)) {
- CharSequence contentCS = extras.getCharSequence(Notification.EXTRA_TEXT);
- if (contentCS != null) {
- notificationSpec.body = contentCS.toString();
- }
+
+ CharSequence contentCS = null;
+ if (preferBigText && extras.containsKey(Notification.EXTRA_BIG_TEXT)) {
+ contentCS = extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
+ } else if (extras.containsKey(Notification.EXTRA_TEXT)) {
+ contentCS = extras.getCharSequence(Notification.EXTRA_TEXT);
}
+ if (contentCS != null) {
+ notificationSpec.body = contentCS.toString();
+ }
+
}
private boolean isServiceRunning() {
@@ -354,7 +340,7 @@ public class NotificationListener extends NotificationListenerService {
}
PlaybackState s = c.getPlaybackState();
- stateSpec.position = (int)s.getPosition();
+ stateSpec.position = (int) (s.getPosition() / 1000);
stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed());
stateSpec.repeat = 1;
stateSpec.shuffle = 1;
@@ -384,6 +370,10 @@ public class NotificationListener extends NotificationListenerService {
musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE);
if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION))
musicSpec.duration = (int)d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000;
+ if (d.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS))
+ musicSpec.trackCount = (int)d.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS);
+ if (d.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER))
+ musicSpec.trackNr = (int)d.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
// finally, tell the device about it
GBApplication.deviceService().onSetMusicInfo(musicSpec);
@@ -396,4 +386,14 @@ public class NotificationListener extends NotificationListenerService {
public void onNotificationRemoved(StatusBarNotification sbn) {
}
+
+ private void dumpExtras(Bundle bundle) {
+ for (String key : bundle.keySet()) {
+ Object value = bundle.get(key);
+ if (value == null) {
+ continue;
+ }
+ LOG.debug(String.format("Notification extra: %s %s (%s)", key, value.toString(), value.getClass().getName()));
+ }
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java
index 801c151bb..723f5c4ad 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java
@@ -58,10 +58,13 @@ public class PebbleReceiver extends BroadcastReceiver {
}
if (notificationSpec.title != null) {
- notificationSpec.type = NotificationType.UNDEFINED;
+ notificationSpec.type = NotificationType.UNKNOWN;
String sender = intent.getStringExtra("sender");
if ("Conversations".equals(sender)) {
- notificationSpec.type = NotificationType.CHAT;
+ notificationSpec.type = NotificationType.CONVERSATIONS;
+ }
+ else if ("OsmAnd".equals(sender)) {
+ notificationSpec.type = NotificationType.GENERIC_NAVIGATION;
}
GBApplication.deviceService().onNotification(notificationSpec);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java
index 5df07c8d6..62e66a265 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java
@@ -31,7 +31,7 @@ public class SMSReceiver extends BroadcastReceiver {
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = -1;
- notificationSpec.type = NotificationType.SMS;
+ notificationSpec.type = NotificationType.GENERIC_SMS;
Bundle bundle = intent.getExtras();
if (bundle != null) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java
deleted file mode 100644
index 895b903b6..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.impl;
-
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
-import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
-
-public class GBActivitySample implements ActivitySample {
- private final int timestamp;
- private final SampleProvider provider;
- private final int intensity;
- private final int steps;
- private final int type;
- private final int customValue;
-
- public GBActivitySample(SampleProvider provider, int timestamp, int intensity, int steps, int type) {
- this(provider, timestamp, intensity, steps, type, 0);
- }
-
- public GBActivitySample(SampleProvider provider, int timestamp, int intensity, int steps, int type, int customValue) {
- this.timestamp = timestamp;
- this.provider = provider;
- this.intensity = intensity;
- this.steps = steps;
- this.customValue = customValue;
- this.type = type;
- validate();
- }
-
- private void validate() {
- if (steps < 0) {
- throw new IllegalArgumentException("steps must be >= 0");
- }
- if (intensity < 0) {
- throw new IllegalArgumentException("intensity must be >= 0");
- }
- if (timestamp < 0) {
- throw new IllegalArgumentException("timestamp must be >= 0");
- }
- if (customValue < 0) {
- throw new IllegalArgumentException("customValue must be >= 0");
- }
- }
-
- @Override
- public int getTimestamp() {
- return timestamp;
- }
-
- @Override
- public SampleProvider getProvider() {
- return provider;
- }
-
- @Override
- public int getRawIntensity() {
- return intensity;
- }
-
- @Override
- public float getIntensity() {
- return getProvider().normalizeIntensity(getRawIntensity());
- }
-
- @Override
- public int getSteps() {
- return steps;
- }
-
- @Override
- public int getRawKind() {
- return type;
- }
-
- @Override
- public int getKind() {
- return getProvider().normalizeType(getRawKind());
- }
-
- @Override
- public int getCustomValue() {
- return customValue;
- }
-
- @Override
- public String toString() {
- return "GBActivitySample{" +
- "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimeStamp(timestamp)) +
- ", intensity=" + getIntensity() +
- ", steps=" + getSteps() +
- ", customValue=" + getCustomValue() +
- ", type=" + getKind() +
- '}';
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java
index 546bcee22..54ac2e310 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java
@@ -27,7 +27,7 @@ public class GBAlarm implements Alarm {
public static final String[] DEFAULT_ALARMS = {"2,false,false,0,15,30", "1,false,false,96,8,0", "0,false,true,31,7,30"};
- public GBAlarm(int index, boolean enabled, boolean smartWakeup, byte repetition, int hour, int minute) {
+ public GBAlarm(int index, boolean enabled, boolean smartWakeup, int repetition, int hour, int minute) {
this.index = index;
this.enabled = enabled;
this.smartWakeup = smartWakeup;
@@ -47,6 +47,10 @@ public class GBAlarm implements Alarm {
this.minute = Integer.parseInt(tokens[5]);
}
+ public static GBAlarm createSingleShot(int index, boolean smartWakeup, Calendar calendar) {
+ return new GBAlarm(index, true, smartWakeup, Alarm.ALARM_ONCE, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE));
+ }
+
private static GBAlarm readFromParcel(Parcel pc) {
int index = pc.readInt();
boolean enabled = Boolean.parseBoolean(pc.readString());
@@ -54,7 +58,7 @@ public class GBAlarm implements Alarm {
int repetition = pc.readInt();
int hour = pc.readInt();
int minute = pc.readInt();
- return new GBAlarm(index, enabled, smartWakeup, (byte) repetition, hour, minute);
+ return new GBAlarm(index, enabled, smartWakeup, repetition, hour, minute);
}
@Override
@@ -149,6 +153,11 @@ public class GBAlarm implements Alarm {
return this.repetition;
}
+ @Override
+ public boolean isRepetitive() {
+ return getRepetitionMask() != ALARM_ONCE;
+ }
+
public String toPreferences() {
return String.valueOf(this.index) + ',' +
String.valueOf(this.enabled) + ',' +
@@ -205,10 +214,12 @@ public class GBAlarm implements Alarm {
}
public static final Creator CREATOR = new Creator() {
+ @Override
public GBAlarm createFromParcel(Parcel in) {
return readFromParcel(in);
}
+ @Override
public GBAlarm[] newArray(int size) {
return new GBAlarm[size];
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java
index 252755342..29f61d76f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import org.slf4j.Logger;
@@ -42,12 +43,16 @@ public class GBDevice implements Parcelable {
public static final String EXTRA_DEVICE = "device";
private static final String DEVINFO_HW_VER = "HW: ";
private static final String DEVINFO_FW_VER = "FW: ";
+ private static final String DEVINFO_HR_VER = "HR: ";
private static final String DEVINFO_ADDR = "ADDR: ";
- private final String mName;
+ private static final String DEVINFO_ADDR2 = "ADDR2: ";
+ private String mName;
private final String mAddress;
+ private String mVolatileAddress;
private final DeviceType mDeviceType;
- private String mFirmwareVersion = null;
- private String mHardwareVersion = null;
+ private String mFirmwareVersion;
+ private String mFirmwareVersion2;
+ private String mModel;
private State mState = State.NOT_CONNECTED;
private short mBatteryLevel = BATTERY_UNKNOWN;
private short mBatteryThresholdPercent = BATTERY_THRESHOLD_PERCENT;
@@ -57,8 +62,13 @@ public class GBDevice implements Parcelable {
private List mDeviceInfos;
public GBDevice(String address, String name, DeviceType deviceType) {
+ this(address, null, name, deviceType);
+ }
+
+ public GBDevice(String address, String address2, String name, DeviceType deviceType) {
mAddress = address;
- mName = name;
+ mVolatileAddress = address2;
+ mName = (name != null) ? name : mAddress;
mDeviceType = deviceType;
validate();
}
@@ -66,9 +76,11 @@ public class GBDevice implements Parcelable {
private GBDevice(Parcel in) {
mName = in.readString();
mAddress = in.readString();
+ mVolatileAddress = in.readString();
mDeviceType = DeviceType.values()[in.readInt()];
mFirmwareVersion = in.readString();
- mHardwareVersion = in.readString();
+ mFirmwareVersion2 = in.readString();
+ mModel = in.readString();
mState = State.values()[in.readInt()];
mBatteryLevel = (short) in.readInt();
mBatteryThresholdPercent = (short) in.readInt();
@@ -84,9 +96,11 @@ public class GBDevice implements Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeString(mAddress);
+ dest.writeString(mVolatileAddress);
dest.writeInt(mDeviceType.ordinal());
dest.writeString(mFirmwareVersion);
- dest.writeString(mHardwareVersion);
+ dest.writeString(mFirmwareVersion2);
+ dest.writeString(mModel);
dest.writeInt(mState.ordinal());
dest.writeInt(mBatteryLevel);
dest.writeInt(mBatteryThresholdPercent);
@@ -106,24 +120,58 @@ public class GBDevice implements Parcelable {
return mName;
}
+ public void setName(String name) {
+ if (name == null) {
+ LOG.warn("Ignoring setting of GBDevice name to null for " + this);
+ return;
+ }
+ mName = name;
+ }
+
public String getAddress() {
return mAddress;
}
+ public String getVolatileAddress() {
+ return mVolatileAddress;
+ }
+
public String getFirmwareVersion() {
return mFirmwareVersion;
}
+ public String getFirmwareVersion2() {
+ return mFirmwareVersion2;
+ }
public void setFirmwareVersion(String firmwareVersion) {
mFirmwareVersion = firmwareVersion;
}
- public String getHardwareVersion() {
- return mHardwareVersion;
+ /**
+ * Sets the second firmware version, typically the heart rate firmware version
+ * @param firmwareVersion2
+ */
+ public void setFirmwareVersion2(String firmwareVersion2) {
+ mFirmwareVersion2 = firmwareVersion2;
}
- public void setHardwareVersion(String hardwareVersion) {
- mHardwareVersion = hardwareVersion;
+ public void setVolatileAddress(String volatileAddress) {
+ mVolatileAddress = volatileAddress;
+ }
+
+ /**
+ * Returns the specific model/hardware revision of this device.
+ * This information is not always available, typically only when the device is initialized
+ * @return the model/hardware revision of this device
+ * @see #getType()
+ */
+ @Nullable
+ public String getModel() {
+ return mModel;
+ }
+
+ public void setModel(String model) {
+ mModel = model;
}
public boolean isConnected() {
@@ -197,6 +245,7 @@ public class GBDevice implements Parcelable {
setBatteryLevel(BATTERY_UNKNOWN);
setBatteryState(BatteryState.UNKNOWN);
setFirmwareVersion(null);
+ setFirmwareVersion2(null);
setRssi(RSSI_UNKNOWN);
if (mBusyTask != null) {
unsetBusyTask();
@@ -204,29 +253,51 @@ public class GBDevice implements Parcelable {
}
public String getStateString() {
- /*
- * for simplicity the user wont see all internal states, just connecting -> connected
- * instead of connecting->connected->initializing->initialized
- */
+ return getStateString(true);
+ }
+
+ /**
+ * for simplicity the user won't see all internal states, just connecting -> connected
+ * instead of connecting->connected->initializing->initialized
+ * Set simple to true to get this behavior.
+ */
+ private String getStateString(boolean simple) {
switch (mState) {
case NOT_CONNECTED:
return GBApplication.getContext().getString(R.string.not_connected);
case WAITING_FOR_RECONNECT:
return GBApplication.getContext().getString(R.string.waiting_for_reconnect);
case CONNECTING:
- case CONNECTED:
- case INITIALIZING:
return GBApplication.getContext().getString(R.string.connecting);
+ case CONNECTED:
+ if (simple) {
+ return GBApplication.getContext().getString(R.string.connecting);
+ }
+ return GBApplication.getContext().getString(R.string.connected);
+ case INITIALIZING:
+ if (simple) {
+ return GBApplication.getContext().getString(R.string.connecting);
+ }
+ return GBApplication.getContext().getString(R.string.initializing);
case AUTHENTICATION_REQUIRED:
return GBApplication.getContext().getString(R.string.authentication_required);
case AUTHENTICATING:
return GBApplication.getContext().getString(R.string.authenticating);
case INITIALIZED:
- return GBApplication.getContext().getString(R.string.connected);
+ if (simple) {
+ return GBApplication.getContext().getString(R.string.connected);
+ }
+ return GBApplication.getContext().getString(R.string.initialized);
}
return GBApplication.getContext().getString(R.string.unknown_state);
}
+ /**
+ * Returns the general type of this device. For more detailed information,
+ * soo #getModel()
+ * @return the general type of this device
+ */
+ @NonNull
public DeviceType getType() {
return mDeviceType;
}
@@ -313,7 +384,7 @@ public class GBDevice implements Parcelable {
@Override
public String toString() {
- return "Device " + getName() + ", " + getAddress() + ", " + getStateString();
+ return "Device " + getName() + ", " + getAddress() + ", " + getStateString(false);
}
/**
@@ -336,20 +407,35 @@ public class GBDevice implements Parcelable {
return getDeviceInfos().size() > 0;
}
+ public ItemWithDetails getDeviceInfo(String name) {
+ for (ItemWithDetails item : getDeviceInfos()) {
+ if (name.equals(item.getName())) {
+ return item;
+ }
+ }
+ return null;
+ }
+
public List getDeviceInfos() {
List result = new ArrayList<>();
if (mDeviceInfos != null) {
result.addAll(mDeviceInfos);
}
- if (mHardwareVersion != null) {
- result.add(new GenericItem(DEVINFO_HW_VER, mHardwareVersion));
+ if (mModel != null) {
+ result.add(new GenericItem(DEVINFO_HW_VER, mModel));
}
if (mFirmwareVersion != null) {
result.add(new GenericItem(DEVINFO_FW_VER, mFirmwareVersion));
}
+ if (mFirmwareVersion2 != null) {
+ result.add(new GenericItem(DEVINFO_HR_VER, mFirmwareVersion2));
+ }
if (mAddress != null) {
result.add(new GenericItem(DEVINFO_ADDR, mAddress));
}
+ if (mVolatileAddress != null) {
+ result.add(new GenericItem(DEVINFO_ADDR2, mVolatileAddress));
+ }
Collections.sort(result);
return result;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java
index f55876453..5cbd0fab0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java
@@ -89,6 +89,7 @@ public class GBDeviceApp {
public enum Type {
UNKNOWN,
WATCHFACE,
+ WATCHFACE_SYSTEM,
APP_GENERIC,
APP_ACTIVITYTRACKER,
APP_SYSTEM,
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java
index d07556360..3696ec5d5 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java
@@ -2,11 +2,24 @@ package nodomain.freeyourgadget.gadgetbridge.impl;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
+import android.os.ParcelUuid;
import android.os.Parcelable;
+import android.support.annotation.NonNull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
/**
* A device candidate is a Bluetooth device that is not yet managed by
@@ -14,23 +27,29 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
* support this candidate, will the candidate be promoted to a GBDevice.
*/
public class GBDeviceCandidate implements Parcelable {
+ private static final Logger LOG = LoggerFactory.getLogger(GBDeviceCandidate.class);
+
private final BluetoothDevice device;
private final short rssi;
+ private final ParcelUuid[] serviceUuds;
private DeviceType deviceType = DeviceType.UNKNOWN;
- public GBDeviceCandidate(BluetoothDevice device, short rssi) {
+ public GBDeviceCandidate(BluetoothDevice device, short rssi, ParcelUuid[] serviceUuds) {
this.device = device;
this.rssi = rssi;
+ this.serviceUuds = mergeServiceUuids(serviceUuds, device.getUuids());
}
private GBDeviceCandidate(Parcel in) {
device = in.readParcelable(getClass().getClassLoader());
+ if (device == null) {
+ throw new IllegalStateException("Unable to read state from Parcel");
+ }
rssi = (short) in.readInt();
deviceType = DeviceType.valueOf(in.readString());
- if (device == null || deviceType == null) {
- throw new IllegalStateException("Unable to read state from Parcel");
- }
+ ParcelUuid[] uuids = AndroidUtils.toParcelUUids(in.readParcelableArray(getClass().getClassLoader()));
+ serviceUuds = mergeServiceUuids(uuids, device.getUuids());
}
@Override
@@ -38,6 +57,27 @@ public class GBDeviceCandidate implements Parcelable {
dest.writeParcelable(device, 0);
dest.writeInt(rssi);
dest.writeString(deviceType.name());
+ dest.writeArray(serviceUuds);
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public GBDeviceCandidate createFromParcel(Parcel in) {
+ return new GBDeviceCandidate(in);
+ }
+
+ @Override
+ public GBDeviceCandidate[] newArray(int size) {
+ return new GBDeviceCandidate[size];
+ }
+ };
+
+ public BluetoothDevice getDevice() {
+ return device;
+ }
+
+ public void setDeviceType(DeviceType type) {
+ deviceType = type;
}
public DeviceType getDeviceType() {
@@ -48,15 +88,54 @@ public class GBDeviceCandidate implements Parcelable {
return device != null ? device.getAddress() : GBApplication.getContext().getString(R.string._unknown_);
}
+ private ParcelUuid[] mergeServiceUuids(ParcelUuid[] serviceUuds, ParcelUuid[] deviceUuids) {
+ Set uuids = new HashSet<>();
+ if (serviceUuds != null) {
+ uuids.addAll(Arrays.asList(serviceUuds));
+ }
+ if (deviceUuids != null) {
+ uuids.addAll(Arrays.asList(deviceUuids));
+ }
+ return uuids.toArray(new ParcelUuid[0]);
+ }
+
+ @NonNull
+ public ParcelUuid[] getServiceUuids() {
+ return serviceUuds;
+ }
+
+ public boolean supportsService(UUID aService) {
+ ParcelUuid[] uuids = getServiceUuids();
+ if (uuids.length == 0) {
+ LOG.warn("no cached services available for " + this);
+ return false;
+ }
+
+ for (ParcelUuid uuid : uuids) {
+ if (uuid != null && aService.equals(uuid.getUuid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public String getName() {
- String name = null;
- if (device != null) {
- name = device.getName();
+ String deviceName = null;
+ try {
+ Method method = device.getClass().getMethod("getAliasName");
+ if (method != null) {
+ deviceName = (String) method.invoke(device);
+ }
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) {
+ LOG.info("Could not get device alias for " + deviceName);
}
- if (name == null || name.length() == 0) {
- name = GBApplication.getContext().getString(R.string._unknown_);
+ if (deviceName == null || deviceName.length() == 0) {
+ deviceName = device.getName();
}
- return name;
+ if (deviceName == null || deviceName.length() == 0) {
+ deviceName = "(unknown)";
+ }
+ return deviceName;
}
public short getRssi() {
@@ -85,4 +164,9 @@ public class GBDeviceCandidate implements Parcelable {
public int hashCode() {
return device.getAddress().hashCode() ^ 37;
}
+
+ @Override
+ public String toString() {
+ return getName() + ": " + getMacAddress();
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
index 60ff7b526..8fd47d311 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
@@ -12,12 +12,15 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
+//import java.util.UUID;
+
public class GBDeviceService implements DeviceService {
protected final Context mContext;
protected final Class extends Service> mServiceClass;
@@ -28,8 +31,7 @@ public class GBDeviceService implements DeviceService {
}
protected Intent createIntent() {
- Intent startIntent = new Intent(mContext, mServiceClass);
- return startIntent;
+ return new Intent(mContext, mServiceClass);
}
protected void invokeService(Intent intent) {
@@ -126,6 +128,14 @@ public class GBDeviceService implements DeviceService {
invokeService(intent);
}
+ @Override
+ public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
+ Intent intent = createIntent().setAction(ACTION_SETCANNEDMESSAGES)
+ .putExtra(EXTRA_CANNEDMESSAGES_TYPE, cannedMessagesSpec.type)
+ .putExtra(EXTRA_CANNEDMESSAGES, cannedMessagesSpec.cannedMessages);
+ invokeService(intent);
+ }
+
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
Intent intent = createIntent().setAction(ACTION_SETMUSICSTATE)
@@ -185,6 +195,13 @@ public class GBDeviceService implements DeviceService {
invokeService(intent);
}
+ @Override
+ public void onAppReorder(UUID[] uuids) {
+ Intent intent = createIntent().setAction(ACTION_APP_REORDER)
+ .putExtra(EXTRA_APP_UUID, uuids);
+ invokeService(intent);
+ }
+
@Override
public void onFetchActivityData() {
Intent intent = createIntent().setAction(ACTION_FETCH_ACTIVITY_DATA);
@@ -210,6 +227,13 @@ public class GBDeviceService implements DeviceService {
invokeService(intent);
}
+ @Override
+ public void onSetConstantVibration(int intensity) {
+ Intent intent = createIntent().setAction(ACTION_SET_CONSTANT_VIBRATION)
+ .putExtra(EXTRA_VIBRATION_INTENSITY, intensity);
+ invokeService(intent);
+ }
+
@Override
public void onScreenshotReq() {
Intent intent = createIntent().setAction(ACTION_REQUEST_SCREENSHOT);
@@ -256,4 +280,17 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_CALENDAREVENT_ID, id);
invokeService(intent);
}
+
+ @Override
+ public void onSendConfiguration(String config) {
+ Intent intent = createIntent().setAction(ACTION_SEND_CONFIGURATION)
+ .putExtra(EXTRA_CONFIG, config);
+ invokeService(intent);
+ }
+
+ @Override
+ public void onTestNewFunction() {
+ Intent intent = createIntent().setAction(ACTION_TEST_NEW_FUNCTION);
+ invokeService(intent);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java
index df7bbce39..e292f4b1e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java
@@ -5,6 +5,7 @@ import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
public class ActivityKind {
+ public static final int TYPE_NOT_MEASURED = -1;
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_ACTIVITY = 1;
public static final int TYPE_LIGHT_SLEEP = 2;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java
index 117a8a7a5..5d97efec9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java
@@ -2,7 +2,26 @@ package nodomain.freeyourgadget.gadgetbridge.model;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-public interface ActivitySample {
+/**
+ * The all-in-one interface for a sample measured by a device with
+ * one or more sensors.
+ *
+ * Each sample is the result of one or more measurements. The values are set for
+ * a specific point in time (see @{link #getTimestamp()}).
+ *
+ * If the sample relates to a user activity (e.g. sleeping, walking, running, ...)
+ * then the activity is provided through @{link #getKind()}.
+ *
+ * Methods will return @{link #NOT_MEASURED} in case no value is available for this
+ * sample.
+ *
+ * The frequency of samples, i.e. the how many samples are recorded per minute, is not specified
+ * and may vary.
+ */
+public interface ActivitySample extends TimeStamped {
+
+ int NOT_MEASURED = -1;
+
/**
* Returns the provider of the data.
*
@@ -10,11 +29,6 @@ public interface ActivitySample {
*/
SampleProvider getProvider();
- /**
- * Timestamp of the sample, resolution is seconds!
- */
- int getTimestamp();
-
/**
* Returns the raw activity kind value as recorded by the SampleProvider
*/
@@ -42,5 +56,20 @@ public interface ActivitySample {
*/
int getSteps();
- int getCustomValue();
+ /**
+ * Returns the heart rate measured at the corresponding timestamp.
+ * The value is returned in heart beats per minute, in the range from
+ * 0-255, where 255 is an illegal value (e.g. due to a bad measurement)
+ *
+ * @return the heart rate value in beats per minute, or -1 if none
+ */
+ int getHeartRate();
+
+ /**
+ * Sets the heart rate value of this sample. Typically only used in
+ * generic db migration.
+ *
+ * @param value the value in bpm
+ */
+ void setHeartRate(int value);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java
index 358c4a8ea..3ed594083 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java
@@ -1,8 +1,12 @@
package nodomain.freeyourgadget.gadgetbridge.model;
import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
/**
@@ -10,50 +14,61 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
*/
public class ActivityUser {
- private Integer activityUserGender;
- private Integer activityUserYearOfBirth;
- private Integer activityUserHeightCm;
- private Integer activityUserWeightKg;
- private Integer activityUserSleepDuration;
+ public static final int GENDER_FEMALE = 0;
+ public static final int GENDER_MALE = 1;
+ public static final int GENDER_OTHER = 2;
- public static final int defaultUserGender = 0;
+ private String activityUserName;
+ private int activityUserGender;
+ private int activityUserYearOfBirth;
+ private int activityUserHeightCm;
+ private int activityUserWeightKg;
+ private int activityUserSleepDuration;
+ private int activityUserStepsGoal;
+
+ private static final String defaultUserName = "gadgetbridge-user";
+ public static final int defaultUserGender = GENDER_FEMALE;
public static final int defaultUserYearOfBirth = 0;
public static final int defaultUserAge = 0;
public static final int defaultUserHeightCm = 175;
public static final int defaultUserWeightKg = 70;
public static final int defaultUserSleepDuration = 7;
+ public static final int defaultUserStepsGoal = 8000;
+ public static final String PREF_USER_NAME = "mi_user_alias";
public static final String PREF_USER_YEAR_OF_BIRTH = "activity_user_year_of_birth";
public static final String PREF_USER_GENDER = "activity_user_gender";
public static final String PREF_USER_HEIGHT_CM = "activity_user_height_cm";
public static final String PREF_USER_WEIGHT_KG = "activity_user_weight_kg";
public static final String PREF_USER_SLEEP_DURATION = "activity_user_sleep_duration";
+ public static final String PREF_USER_STEPS_GOAL = MiBandConst.PREF_MIBAND_FITNESS_GOAL;
- public int getActivityUserWeightKg() {
- if (activityUserWeightKg == null) {
- fetchPreferences();
- }
+ public ActivityUser() {
+ fetchPreferences();
+ }
+
+ public String getName() {
+ return activityUserName;
+ }
+
+ public int getWeightKg() {
return activityUserWeightKg;
}
- public int getActivityUserGender() {
- if (activityUserGender == null) {
- fetchPreferences();
- }
+ /**
+ * @see #GENDER_FEMALE
+ * @see #GENDER_MALE
+ * @see #GENDER_OTHER
+ */
+ public int getGender() {
return activityUserGender;
}
- public int getActivityUserYearOfBirth() {
- if (activityUserYearOfBirth == null) {
- fetchPreferences();
- }
+ public int getYearOfBirth() {
return activityUserYearOfBirth;
}
- public int getActivityUserHeightCm() {
- if (activityUserHeightCm == null) {
- fetchPreferences();
- }
+ public int getHeightCm() {
return activityUserHeightCm;
}
@@ -61,18 +76,22 @@ public class ActivityUser {
* @return the user defined sleep duration or the default value when none is set or the stored
* value is out of any logical bounds.
*/
- public int getActivityUserSleepDuration() {
- if (activityUserSleepDuration == null) {
- fetchPreferences();
- }
+ public int getSleepDuration() {
if (activityUserSleepDuration < 1 || activityUserSleepDuration > 24) {
activityUserSleepDuration = defaultUserSleepDuration;
}
return activityUserSleepDuration;
}
- public int getActivityUserAge() {
- int userYear = getActivityUserYearOfBirth();
+ public int getStepsGoal() {
+ if (activityUserStepsGoal < 0) {
+ activityUserStepsGoal = defaultUserStepsGoal;
+ }
+ return activityUserStepsGoal;
+ }
+
+ public int getAge() {
+ int userYear = getYearOfBirth();
int age = 25;
if (userYear > 1900) {
age = Calendar.getInstance().get(Calendar.YEAR) - userYear;
@@ -85,10 +104,18 @@ public class ActivityUser {
private void fetchPreferences() {
Prefs prefs = GBApplication.getPrefs();
+ activityUserName = prefs.getString(PREF_USER_NAME, defaultUserName);
activityUserGender = prefs.getInt(PREF_USER_GENDER, defaultUserGender);
activityUserHeightCm = prefs.getInt(PREF_USER_HEIGHT_CM, defaultUserHeightCm);
activityUserWeightKg = prefs.getInt(PREF_USER_WEIGHT_KG, defaultUserWeightKg);
activityUserYearOfBirth = prefs.getInt(PREF_USER_YEAR_OF_BIRTH, defaultUserYearOfBirth);
activityUserSleepDuration = prefs.getInt(PREF_USER_SLEEP_DURATION, defaultUserSleepDuration);
+ activityUserStepsGoal = prefs.getInt(PREF_USER_STEPS_GOAL, defaultUserStepsGoal);
+ }
+
+ public Date getUserBirthday() {
+ Calendar cal = DateTimeUtils.getCalendarUTC();
+ cal.set(GregorianCalendar.YEAR, getYearOfBirth());
+ return cal.getTime();
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java
index 01adde47a..1f2e461db 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java
@@ -26,5 +26,7 @@ public interface Alarm extends Parcelable, Comparable {
int getRepetitionMask();
+ boolean isRepetitive();
+
boolean getRepetition(int dow);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java
new file mode 100644
index 000000000..001bb6cfd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java
@@ -0,0 +1,58 @@
+package nodomain.freeyourgadget.gadgetbridge.model;
+
+import java.util.HashMap;
+
+public class AppNotificationType extends HashMap {
+
+ private static AppNotificationType _instance;
+
+ public static AppNotificationType getInstance() {
+ if(_instance == null) {
+ return (_instance = new AppNotificationType());
+ }
+
+ return _instance;
+ }
+
+ private AppNotificationType() {
+ // Generic Email
+ put("com.fsck.k9", NotificationType.GENERIC_EMAIL);
+ put("com.android.email", NotificationType.GENERIC_EMAIL);
+
+ // Generic SMS
+ put("com.moez.QKSMS", NotificationType.GENERIC_SMS);
+ put("com.android.mms", NotificationType.GENERIC_SMS);
+ put("com.android.messaging", NotificationType.GENERIC_SMS);
+ put("com.sonyericsson.conversations", NotificationType.GENERIC_SMS);
+ put("org.smssecure.smssecure", NotificationType.GENERIC_SMS);
+
+ // Conversations
+ put("eu.siacs.conversations", NotificationType.CONVERSATIONS);
+
+ // Signal
+ put("org.thoughtcrime.securesms", NotificationType.SIGNAL);
+
+ // Telegram
+ put("org.telegram.messenger", NotificationType.TELEGRAM);
+
+ // Twitter
+ put("org.mariotaku.twidere", NotificationType.TWITTER);
+ put("com.twitter.android", NotificationType.TWITTER);
+ put("org.andstatus.app", NotificationType.TWITTER);
+ put("org.mustard.android", NotificationType.TWITTER);
+
+ // Facebook
+ put("me.zeeroooo.materialfb", NotificationType.FACEBOOK);
+ put("it.rignanese.leo.slimfacebook", NotificationType.FACEBOOK);
+ put("me.jakelane.wrapperforfacebook", NotificationType.FACEBOOK);
+ put("com.facebook.katana", NotificationType.FACEBOOK);
+ put("org.indywidualni.fblite", NotificationType.FACEBOOK);
+
+ // Facebook Messenger
+ put("com.facebook.orca", NotificationType.FACEBOOK_MESSENGER);
+
+ // WhatsApp
+ put("com.whatsapp", NotificationType.WHATSAPP);
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CannedMessagesSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CannedMessagesSpec.java
new file mode 100644
index 000000000..5975cf7b8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CannedMessagesSpec.java
@@ -0,0 +1,10 @@
+package nodomain.freeyourgadget.gadgetbridge.model;
+
+public class CannedMessagesSpec {
+ public static final byte TYPE_GENERIC = 0;
+ public static final byte TYPE_MISSEDCALLS = 1;
+ public static final byte TYPE_NEWSMS = 2;
+
+ public int type;
+ public String[] cannedMessages;
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
index b3921b1b8..82fd39582 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
@@ -12,10 +12,12 @@ import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
public interface DeviceService extends EventHandler {
String PREFIX = "nodomain.freeyourgadget.gadgetbridge.devices";
+ String ACTION_MIBAND2_AUTH = PREFIX + ".action.miban2_auth";
String ACTION_START = PREFIX + ".action.start";
String ACTION_CONNECT = PREFIX + ".action.connect";
String ACTION_NOTIFICATION = PREFIX + ".action.notification";
String ACTION_CALLSTATE = PREFIX + ".action.callstate";
+ String ACTION_SETCANNEDMESSAGES = PREFIX + ".action.setcannedmessages";
String ACTION_SETTIME = PREFIX + ".action.settime";
String ACTION_SETMUSICINFO = PREFIX + ".action.setmusicinfo";
String ACTION_SETMUSICSTATE = PREFIX + ".action.setmusicstate";
@@ -25,20 +27,29 @@ public interface DeviceService extends EventHandler {
String ACTION_STARTAPP = PREFIX + ".action.startapp";
String ACTION_DELETEAPP = PREFIX + ".action.deleteapp";
String ACTION_APP_CONFIGURE = PREFIX + ".action.app_configure";
+ String ACTION_APP_REORDER = PREFIX + ".action.app_reorder";
String ACTION_INSTALL = PREFIX + ".action.install";
String ACTION_REBOOT = PREFIX + ".action.reboot";
String ACTION_HEARTRATE_TEST = PREFIX + ".action.heartrate_test";
String ACTION_FETCH_ACTIVITY_DATA = PREFIX + ".action.fetch_activity_data";
String ACTION_DISCONNECT = PREFIX + ".action.disconnect";
String ACTION_FIND_DEVICE = PREFIX + ".action.find_device";
+ String ACTION_SET_CONSTANT_VIBRATION = PREFIX + ".action.set_constant_vibration";
String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms";
String ACTION_ENABLE_REALTIME_STEPS = PREFIX + ".action.enable_realtime_steps";
+ String ACTION_REALTIME_SAMPLES = PREFIX + ".action.realtime_samples";
+ /**
+ * Use EXTRA_REALTIME_SAMPLE instead
+ */
+ @Deprecated
String ACTION_REALTIME_STEPS = PREFIX + ".action.realtime_steps";
String ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT = PREFIX + ".action.realtime_hr_measurement";
String ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT = PREFIX + ".action.enable_heartrate_sleep_support";
String ACTION_HEARTRATE_MEASUREMENT = PREFIX + ".action.hr_measurement";
String ACTION_ADD_CALENDAREVENT = PREFIX + ".action.add_calendarevent";
String ACTION_DELETE_CALENDAREVENT = PREFIX + ".action.delete_calendarevent";
+ String ACTION_SEND_CONFIGURATION = PREFIX + ".action.send_configuration";
+ String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function";
String EXTRA_DEVICE_ADDRESS = "device_address";
String EXTRA_NOTIFICATION_BODY = "notification_body";
String EXTRA_NOTIFICATION_FLAGS = "notification_flags";
@@ -50,8 +61,11 @@ public interface DeviceService extends EventHandler {
String EXTRA_NOTIFICATION_TITLE = "notification_title";
String EXTRA_NOTIFICATION_TYPE = "notification_type";
String EXTRA_FIND_START = "find_start";
+ String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";
String EXTRA_CALL_COMMAND = "call_command";
String EXTRA_CALL_PHONENUMBER = "call_phonenumber";
+ String EXTRA_CANNEDMESSAGES = "cannedmessages";
+ String EXTRA_CANNEDMESSAGES_TYPE = "cannedmessages_type";
String EXTRA_MUSIC_ARTIST = "music_artist";
String EXTRA_MUSIC_ALBUM = "music_album";
String EXTRA_MUSIC_TRACK = "music_track";
@@ -67,11 +81,21 @@ public interface DeviceService extends EventHandler {
String EXTRA_APP_START = "app_start";
String EXTRA_APP_CONFIG = "app_config";
String EXTRA_URI = "uri";
+ String EXTRA_CONFIG = "config";
String EXTRA_ALARMS = "alarms";
String EXTRA_PERFORM_PAIR = "perform_pair";
String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps";
+ /**
+ * Use EXTRA_REALTIME_SAMPLE instead
+ */
+ @Deprecated
String EXTRA_REALTIME_STEPS = "realtime_steps";
+ String EXTRA_REALTIME_SAMPLE = "realtime_sample";
String EXTRA_TIMESTAMP = "timestamp";
+ /**
+ * Use EXTRA_REALTIME_SAMPLE instead
+ */
+ @Deprecated
String EXTRA_HEART_RATE_VALUE = "hr_value";
String EXTRA_CALENDAREVENT_ID = "calendarevent_id";
String EXTRA_CALENDAREVENT_TYPE = "calendarevent_type";
@@ -79,6 +103,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_CALENDAREVENT_DURATION = "calendarevent_duration";
String EXTRA_CALENDAREVENT_TITLE = "calendarevent_title";
String EXTRA_CALENDAREVENT_DESCRIPTION = "calendarevent_description";
+ String EXTRA_MIBAND2_AUTH_BYTE = "miband2_auth_byte";
void start();
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index e7b357af7..5c9d26636 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -2,10 +2,39 @@ package nodomain.freeyourgadget.gadgetbridge.model;
/**
* For every supported device, a device type constant must exist.
+ *
+ * Note: they key of every constant is stored in the DB, so it is fixed forever,
+ * and may not be changed.
*/
public enum DeviceType {
- UNKNOWN,
- PEBBLE,
- TEST,
- MIBAND
+ UNKNOWN(-1),
+ PEBBLE(1),
+ MIBAND(10),
+ MIBAND2(11),
+ VIBRATISSIMO(20),
+ LIVEVIEW(30),
+ TEST(1000);
+
+ private final int key;
+
+ DeviceType(int key) {
+ this.key = key;
+ }
+
+ public int getKey() {
+ return key;
+ }
+
+ public boolean isSupported() {
+ return this != UNKNOWN;
+ }
+
+ public static DeviceType fromKey(int key) {
+ for (DeviceType type : values()) {
+ if (type.key == key) {
+ return type;
+ }
+ }
+ return DeviceType.UNKNOWN;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java
index af52f02f1..aa9f67ca9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java
@@ -1,5 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.model;
+import java.util.Objects;
+
public class MusicSpec {
public static final int MUSIC_UNDEFINED = 0;
public static final int MUSIC_PLAY = 1;
@@ -14,4 +16,23 @@ public class MusicSpec {
public int duration;
public int trackCount;
public int trackNr;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof MusicSpec)) {
+ return false;
+ }
+ MusicSpec musicSpec = (MusicSpec) obj;
+
+ return Objects.equals(this.artist, musicSpec.artist) &&
+ Objects.equals(this.album, musicSpec.album) &&
+ Objects.equals(this.track, musicSpec.track) &&
+ this.duration == musicSpec.duration &&
+ this.trackCount == musicSpec.trackCount &&
+ this.trackNr == musicSpec.trackNr;
+
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java
index ee5510fbd..fb15948d7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java
@@ -14,4 +14,21 @@ public class MusicStateSpec {
public int playRate;
public byte shuffle;
public byte repeat;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof MusicStateSpec)) {
+ return false;
+ }
+ MusicStateSpec stateSpec = (MusicStateSpec) obj;
+
+ return this.state == stateSpec.state &&
+ Math.abs(this.position - stateSpec.position)<=2 &&
+ this.playRate == stateSpec.playRate &&
+ this.shuffle == stateSpec.shuffle &&
+ this.repeat == stateSpec.repeat;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java
index 63feca893..e3e32070f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java
@@ -1,12 +1,58 @@
package nodomain.freeyourgadget.gadgetbridge.model;
+import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor;
+import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleIconID;
+
public enum NotificationType {
- UNDEFINED,
+ UNKNOWN(PebbleIconID.NOTIFICATION_GENERIC, PebbleColor.Red),
- CHAT,
- EMAIL,
- FACEBOOK,
- SMS,
- TWITTER,
+ CONVERSATIONS(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.Inchworm),
+ GENERIC_EMAIL(PebbleIconID.GENERIC_EMAIL, PebbleColor.JaegerGreen),
+ GENERIC_NAVIGATION(PebbleIconID.LOCATION, PebbleColor.Orange),
+ GENERIC_SMS(PebbleIconID.GENERIC_SMS, PebbleColor.VividViolet),
+ FACEBOOK(PebbleIconID.NOTIFICATION_FACEBOOK, PebbleColor.Liberty),
+ FACEBOOK_MESSENGER(PebbleIconID.NOTIFICATION_FACEBOOK_MESSENGER, PebbleColor.VeryLightBlue),
+ SIGNAL(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.BlueMoon),
+ TWITTER(PebbleIconID.NOTIFICATION_TWITTER, PebbleColor.BlueMoon),
+ TELEGRAM(PebbleIconID.NOTIFICATION_TELEGRAM, PebbleColor.PictonBlue),
+ WHATSAPP(PebbleIconID.NOTIFICATION_WHATSAPP, PebbleColor.MayGreen);
+
+ public int icon;
+ public byte color;
+
+ NotificationType(int icon, byte color) {
+ this.icon = icon;
+ this.color = color;
+ }
+
+ /**
+ * Returns the enum constant as a fixed String value, e.g. to be used
+ * as preference key. In case the keys are ever changed, this method
+ * may be used to bring backward compatibility.
+ */
+ public String getFixedValue() {
+ return name().toLowerCase();
+ }
+
+ public String getGenericType() {
+ switch (this) {
+ case GENERIC_EMAIL:
+ case GENERIC_NAVIGATION:
+ case GENERIC_SMS:
+ return getFixedValue();
+ case FACEBOOK:
+ case TWITTER:
+ return "generic_social";
+ case CONVERSATIONS:
+ case FACEBOOK_MESSENGER:
+ case SIGNAL:
+ case TELEGRAM:
+ case WHATSAPP:
+ return "generic_chat";
+ case UNKNOWN:
+ default:
+ return "generic";
+ }
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeStamped.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeStamped.java
new file mode 100644
index 000000000..b295f798d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeStamped.java
@@ -0,0 +1,8 @@
+package nodomain.freeyourgadget.gadgetbridge.model;
+
+public interface TimeStamped {
+ /**
+ * Unix timestamp of the sample, i.e. the number of seconds since 1970-01-01 00:00:00 UTC.
+ */
+ int getTimestamp();
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ValidByDate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ValidByDate.java
new file mode 100644
index 000000000..2c634d6ee
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ValidByDate.java
@@ -0,0 +1,8 @@
+package nodomain.freeyourgadget.gadgetbridge.model;
+
+import java.util.Date;
+
+public interface ValidByDate {
+ Date getValidFromUTC();
+ Date getValidToUTC();
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
index 21d7965b9..ef37a75b7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
@@ -24,7 +24,7 @@ import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
-import nodomain.freeyourgadget.gadgetbridge.activities.AppManagerActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsHost;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
@@ -61,6 +61,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
private Context context;
private boolean autoReconnect;
+ @Override
public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) {
this.gbDevice = gbDevice;
this.btAdapter = btAdapter;
@@ -152,7 +153,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
return;
}
gbDevice.setFirmwareVersion(infoEvent.fwVersion);
- gbDevice.setHardwareVersion(infoEvent.hwVersion);
+ gbDevice.setModel(infoEvent.hwVersion);
gbDevice.sendDeviceUpdateIntent(context);
}
@@ -160,7 +161,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
Context context = getContext();
LOG.info("Got event for APP_INFO");
- Intent appInfoIntent = new Intent(AppManagerActivity.ACTION_REFRESH_APPLIST);
+ Intent appInfoIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST);
int appCount = appInfoEvent.apps.length;
appInfoIntent.putExtra("app_count", appCount);
for (Integer i = 0; i < appCount; i++) {
@@ -243,10 +244,12 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
action = NotificationListener.ACTION_MUTE;
break;
case REPLY:
- String phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(deviceEvent.handle);
- if (phoneNumber != null) {
- LOG.info("got notfication reply for SMS from " + phoneNumber + " : " + deviceEvent.reply);
- SmsManager.getDefault().sendTextMessage(phoneNumber, null, deviceEvent.reply, null, null);
+ if (deviceEvent.phoneNumber == null) {
+ deviceEvent.phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(deviceEvent.handle);
+ }
+ if (deviceEvent.phoneNumber != null) {
+ LOG.info("got notfication reply for SMS from " + deviceEvent.phoneNumber + " : " + deviceEvent.reply);
+ SmsManager.getDefault().sendTextMessage(deviceEvent.phoneNumber, null, deviceEvent.reply, null, null);
} else {
LOG.info("got notfication reply for notification id " + deviceEvent.handle + " : " + deviceEvent.reply);
action = NotificationListener.ACTION_REPLY;
@@ -279,11 +282,11 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
(BatteryState.BATTERY_LOW.equals(deviceEvent.state) ||
BatteryState.BATTERY_NORMAL.equals(deviceEvent.state))
) {
- GB.updateBatteryNotification(context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), deviceEvent.level),
+ GB.updateBatteryNotification(context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)),
deviceEvent.extendedInfoAvailable() ?
- context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), deviceEvent.level) + "\n" +
+ context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)) + "\n" +
context.getString(R.string.notif_battery_low_bigtext_last_charge_time, DateFormat.getDateTimeInstance().format(deviceEvent.lastChargeTime.getTime())) +
- context.getString(R.string.notif_battery_low_bigtext_number_of_charges, deviceEvent.numCharges)
+ context.getString(R.string.notif_battery_low_bigtext_number_of_charges, String.valueOf(deviceEvent.numCharges))
: ""
, context);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
index 7903ff22a..8c25a793f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
@@ -25,6 +25,10 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.OnboardingActivity;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.K9Receiver;
@@ -37,6 +41,8 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -48,6 +54,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ADD_CALENDAREVENT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_CONFIGURE;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_REORDER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CALLSTATE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP;
@@ -65,12 +72,16 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RE
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_CONFIGURATION;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETCANNEDMESSAGES;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICSTATE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONSTANT_VIBRATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_ALARMS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_CONFIG;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_START;
@@ -84,6 +95,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAL
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_PHONENUMBER;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES_TYPE;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_DEVICE_ADDRESS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM;
@@ -108,9 +122,11 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOT
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_PERFORM_PAIR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_VIBRATION_INTENSITY;
public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class);
+ private static DeviceSupportFactory DEVICE_SUPPORT_FACTORY = null;
private boolean mStarted = false;
@@ -129,6 +145,19 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private Random mRandom = new Random();
+ /**
+ * For testing!
+ *
+ * @param factory
+ */
+ public static void setDeviceSupportFactory(DeviceSupportFactory factory) {
+ DEVICE_SUPPORT_FACTORY = factory;
+ }
+
+ public DeviceCommunicationService() {
+
+ }
+
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -140,6 +169,27 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
boolean enableReceivers = mDeviceSupport != null && (mDeviceSupport.useAutoConnect() || mGBDevice.isInitialized());
setReceiversEnableState(enableReceivers);
GB.updateNotification(mGBDevice.getName() + " " + mGBDevice.getStateString(), mGBDevice.isInitialized(), context);
+
+ if (device.isInitialized()) {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DaoSession session = dbHandler.getDaoSession();
+ boolean askForDBMigration = false;
+ if (DBHelper.findDevice(device, session) == null && device.getType() != DeviceType.VIBRATISSIMO && (device.getType() != DeviceType.LIVEVIEW)) {
+ askForDBMigration = true;
+ }
+ DBHelper.getDevice(device, session); // implicitly creates the device in database if not present, and updates device attributes
+ if (askForDBMigration) {
+ DBHelper dbHelper = new DBHelper(context);
+ if (dbHelper.getOldActivityDatabaseHandler() != null) {
+ Intent startIntent = new Intent(context, OnboardingActivity.class);
+ startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
+ startActivity(startIntent);
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ }
} else {
LOG.error("Got ACTION_DEVICE_CHANGED from unexpected device: " + mGBDevice);
}
@@ -152,13 +202,20 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
LOG.debug("DeviceCommunicationService is being created");
super.onCreate();
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED));
- mFactory = new DeviceSupportFactory(this);
+ mFactory = getDeviceSupportFactory();
if (hasPrefs()) {
getPrefs().getPreferences().registerOnSharedPreferenceChangeListener(this);
}
}
+ private DeviceSupportFactory getDeviceSupportFactory() {
+ if (DEVICE_SUPPORT_FACTORY != null) {
+ return DEVICE_SUPPORT_FACTORY;
+ }
+ return new DeviceSupportFactory(this);
+ }
+
@Override
public synchronized int onStartCommand(Intent intent, int flags, int startId) {
@@ -261,14 +318,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0);
notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME);
- if (notificationSpec.type == NotificationType.SMS && notificationSpec.phoneNumber != null) {
+ if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
notificationSpec.sender = getContactDisplayNameByNumber(notificationSpec.phoneNumber);
notificationSpec.id = mRandom.nextInt(); // FIXME: add this in external SMS Receiver?
GBApplication.getIDSenderLookup().add(notificationSpec.id, notificationSpec.phoneNumber);
}
if (((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) > 0)
- || (notificationSpec.type == NotificationType.SMS && notificationSpec.phoneNumber != null)) {
+ || (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null)) {
// NOTE: maybe not where it belongs
if (prefs.getBoolean("pebble_force_untested", false)) {
// I would rather like to save that as an array in ShadredPreferences
@@ -330,6 +387,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mDeviceSupport.onFindDevice(start);
break;
}
+ case ACTION_SET_CONSTANT_VIBRATION: {
+ int intensity = intent.getIntExtra(EXTRA_VIBRATION_INTENSITY, 0);
+ mDeviceSupport.onSetConstantVibration(intensity);
+ break;
+ }
case ACTION_CALLSTATE:
int command = intent.getIntExtra(EXTRA_CALL_COMMAND, CallSpec.CALL_UNDEFINED);
@@ -345,6 +407,15 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
callSpec.name = callerName;
mDeviceSupport.onSetCallState(callSpec);
break;
+ case ACTION_SETCANNEDMESSAGES:
+ int type = intent.getIntExtra(EXTRA_CANNEDMESSAGES_TYPE, -1);
+ String[] cannedMessages = intent.getStringArrayExtra(EXTRA_CANNEDMESSAGES);
+
+ CannedMessagesSpec cannedMessagesSpec = new CannedMessagesSpec();
+ cannedMessagesSpec.type = type;
+ cannedMessagesSpec.cannedMessages = cannedMessages;
+ mDeviceSupport.onSetCannedMessages(cannedMessagesSpec);
+ break;
case ACTION_SETTIME:
mDeviceSupport.onSetTime();
break;
@@ -360,11 +431,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
break;
case ACTION_SETMUSICSTATE:
MusicStateSpec stateSpec = new MusicStateSpec();
- stateSpec.shuffle = intent.getByteExtra(EXTRA_MUSIC_SHUFFLE, (byte)0);
- stateSpec.repeat = intent.getByteExtra(EXTRA_MUSIC_REPEAT, (byte)0);
+ stateSpec.shuffle = intent.getByteExtra(EXTRA_MUSIC_SHUFFLE, (byte) 0);
+ stateSpec.repeat = intent.getByteExtra(EXTRA_MUSIC_REPEAT, (byte) 0);
stateSpec.position = intent.getIntExtra(EXTRA_MUSIC_POSITION, 0);
stateSpec.playRate = intent.getIntExtra(EXTRA_MUSIC_RATE, 0);
- stateSpec.state = intent.getByteExtra(EXTRA_MUSIC_STATE, (byte)0);
+ stateSpec.state = intent.getByteExtra(EXTRA_MUSIC_STATE, (byte) 0);
mDeviceSupport.onSetMusicState(stateSpec);
break;
case ACTION_REQUEST_APPINFO:
@@ -388,6 +459,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
UUID uuid = (UUID) intent.getSerializableExtra(EXTRA_APP_UUID);
String config = intent.getStringExtra(EXTRA_APP_CONFIG);
mDeviceSupport.onAppConfiguration(uuid, config);
+ break;
+ }
+ case ACTION_APP_REORDER: {
+ UUID[] uuids = (UUID[]) intent.getSerializableExtra(EXTRA_APP_UUID);
+ mDeviceSupport.onAppReorder(uuids);
+ break;
}
case ACTION_INSTALL:
Uri uri = intent.getParcelableExtra(EXTRA_URI);
@@ -415,20 +492,20 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mDeviceSupport.onEnableRealtimeHeartRateMeasurement(enable);
break;
}
+ case ACTION_SEND_CONFIGURATION: {
+ String config = intent.getStringExtra(EXTRA_CONFIG);
+ mDeviceSupport.onSendConfiguration(config);
+ break;
+ }
+ case ACTION_TEST_NEW_FUNCTION: {
+ mDeviceSupport.onTestNewFunction();
+ break;
+ }
}
return START_STICKY;
}
- /**
- * For testing!
- *
- * @param factory
- */
- public void setDeviceSupportFactory(DeviceSupportFactory factory) {
- mFactory = factory;
- }
-
/**
* Disposes the current DeviceSupport instance (if any) and sets a new device support instance
* (if not null).
@@ -567,7 +644,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
setDeviceSupport(null);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification wont be cancelled when service stops
+ nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification won't be cancelled when service stops
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
index 6bc077896..7b3098186 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
@@ -10,8 +10,11 @@ import java.util.EnumSet;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DeviceSupportFactory {
@@ -84,13 +87,22 @@ public class DeviceSupportFactory {
case MIBAND:
deviceSupport = new ServiceDeviceSupport(new MiBandSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
+ case MIBAND2:
+ deviceSupport = new ServiceDeviceSupport(new MiBand2Support(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
+ break;
+ case VIBRATISSIMO:
+ deviceSupport = new ServiceDeviceSupport(new VibratissimoSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
+ break;
+ case LIVEVIEW:
+ deviceSupport = new ServiceDeviceSupport(new LiveviewSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
+ break;
}
if (deviceSupport != null) {
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);
return deviceSupport;
}
} catch (Exception e) {
- throw new GBException(mContext.getString(R.string.cannot_connect_bt_address_invalid_, e));
+ throw new GBException(mContext.getString(R.string.cannot_connect_bt_address_invalid_), e);
}
}
return null;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
index e284f83d8..a90ee63c5 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
@@ -15,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -151,6 +152,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onSetCallState(callSpec);
}
+ @Override
+ public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
+ if (checkBusy("set canned messages")) {
+ return;
+ }
+ delegate.onSetCannedMessages(cannedMessagesSpec);
+ }
+
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
if (checkBusy("set music state")) {
@@ -207,6 +216,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onAppConfiguration(uuid, config);
}
+ @Override
+ public void onAppReorder(UUID[] uuids) {
+ if (checkBusy("app reorder")) {
+ return;
+ }
+ delegate.onAppReorder(uuids);
+ }
+
@Override
public void onFetchActivityData() {
if (checkBusy("fetch activity data")) {
@@ -239,6 +256,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onFindDevice(start);
}
+ @Override
+ public void onSetConstantVibration(int intensity) {
+ if (checkBusy("set constant vibration")) {
+ return;
+ }
+ delegate.onSetConstantVibration(intensity);
+ }
+
@Override
public void onScreenshotReq() {
if (checkBusy("request screenshot")) {
@@ -294,4 +319,20 @@ public class ServiceDeviceSupport implements DeviceSupport {
}
delegate.onDeleteCalendarEvent(type, id);
}
+
+ @Override
+ public void onSendConfiguration(String config) {
+ if (checkBusy("send configuration: " + config)) {
+ return;
+ }
+ delegate.onSendConfiguration(config);
+ }
+
+ @Override
+ public void onTestNewFunction() {
+ if (checkBusy("test new function event")) {
+ return;
+ }
+ delegate.onTestNewFunction();
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
index 261181128..e87c50e32 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
@@ -6,17 +6,19 @@ import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
+import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
/**
* Abstract base class for all devices connected through Bluetooth Low Energy (LE) aka
@@ -30,14 +32,21 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitialize
* @see BtLEQueue
*/
public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback {
- private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class);
-
private BtLEQueue mQueue;
private HashMap mAvailableCharacteristics;
private final Set mSupportedServices = new HashSet<>(4);
+ private Logger logger;
+ private final List> mSupportedProfiles = new ArrayList<>();
public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb"; //this is common for all BTLE devices. see http://stackoverflow.com/questions/18699251/finding-out-android-bluetooth-le-gatt-profiles
+ public AbstractBTLEDeviceSupport(Logger logger) {
+ this.logger = logger;
+ if (logger == null) {
+ throw new IllegalArgumentException("logger must not be null");
+ }
+ }
+
@Override
public boolean connect() {
if (mQueue == null) {
@@ -73,7 +82,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
}
}
- protected TransactionBuilder createTransactionBuilder(String taskName) {
+ public TransactionBuilder createTransactionBuilder(String taskName) {
return new TransactionBuilder(taskName);
}
@@ -107,7 +116,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
* @throws IOException
* @see {@link #performInitialized(String)}
*/
- protected void performConnected(Transaction transaction) throws IOException {
+ public void performConnected(Transaction transaction) throws IOException {
if (!isConnected()) {
if (!connect()) {
throw new IOException("2: Unable to connect to device: " + getDevice());
@@ -116,6 +125,19 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
getQueue().add(transaction);
}
+ /**
+ * Performs the actions of the given transaction as soon as possible,
+ * that is, before any other queued transactions, but after the actions
+ * of the currently executing transaction.
+ * @param builder
+ */
+ public void performImmediately(TransactionBuilder builder) throws IOException {
+ if (!isConnected()) {
+ throw new IOException("Not connected to device: " + getDevice());
+ }
+ getQueue().insert(builder.getTransaction());
+ }
+
public BtLEQueue getQueue() {
return mQueue;
}
@@ -131,6 +153,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
mSupportedServices.add(aSupportedService);
}
+ protected void addSupportedProfile(AbstractBleProfile> profile) {
+ mSupportedProfiles.add(profile);
+ }
+
/**
* Returns the characteristic matching the given UUID. Only characteristics
* are returned whose service is marked as supported.
@@ -148,22 +174,27 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
private void gattServicesDiscovered(List discoveredGattServices) {
if (discoveredGattServices == null) {
+ logger.warn("No gatt services discovered: null!");
return;
}
Set supportedServices = getSupportedServices();
mAvailableCharacteristics = new HashMap<>();
for (BluetoothGattService service : discoveredGattServices) {
if (supportedServices.contains(service.getUuid())) {
+ logger.debug("discovered supported service: " + BleNamesResolver.resolveServiceName(service.getUuid().toString()) + ": " + service.getUuid());
List characteristics = service.getCharacteristics();
if (characteristics == null || characteristics.isEmpty()) {
- LOG.warn("Supported LE service " + service.getUuid() + "did not return any characteristics");
+ logger.warn("Supported LE service " + service.getUuid() + "did not return any characteristics");
continue;
}
HashMap intmAvailableCharacteristics = new HashMap<>(characteristics.size());
for (BluetoothGattCharacteristic characteristic : characteristics) {
intmAvailableCharacteristics.put(characteristic.getUuid(), characteristic);
+ logger.info(" characteristic: " + BleNamesResolver.resolveCharacteristicName(characteristic.getUuid().toString()) + ": " + characteristic.getUuid());
}
mAvailableCharacteristics.putAll(intmAvailableCharacteristics);
+ } else {
+ logger.debug("discovered unsupported service: " + BleNamesResolver.resolveServiceName(service.getUuid().toString()) + ": " + service.getUuid());
}
}
}
@@ -172,9 +203,22 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
return mSupportedServices;
}
+ /**
+ * Utility method that may be used to log incoming messages when we don't know how to deal with them yet.
+ *
+ * @param value
+ */
+ public void logMessageContent(byte[] value) {
+ logger.info("RECEIVED DATA WITH LENGTH: " + ((value != null) ? value.length : "(null)"));
+ Logging.logBytes(logger, value);
+ }
+
// default implementations of event handler methods (gatt callbacks)
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ for (AbstractBleProfile profile : mSupportedProfiles) {
+ profile.onConnectionStateChange(gatt, status, newState);
+ }
}
@Override
@@ -184,29 +228,62 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
}
@Override
- public void onCharacteristicRead(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic, int status) {
+ public boolean onCharacteristicRead(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ for (AbstractBleProfile profile : mSupportedProfiles) {
+ if (profile.onCharacteristicRead(gatt, characteristic, status)) {
+ return true;
+ }
+ }
+ return false;
}
@Override
- public void onCharacteristicWrite(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic, int status) {
+ public boolean onCharacteristicWrite(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ for (AbstractBleProfile profile : mSupportedProfiles) {
+ if (profile.onCharacteristicWrite(gatt, characteristic, status)) {
+ return true;
+ }
+ }
+ return false;
}
@Override
- public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ for (AbstractBleProfile profile : mSupportedProfiles) {
+ if (profile.onDescriptorRead(gatt, descriptor, status)) {
+ return true;
+ }
+ }
+ return false;
}
@Override
- public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ for (AbstractBleProfile profile : mSupportedProfiles) {
+ if (profile.onDescriptorWrite(gatt, descriptor, status)) {
+ return true;
+ }
+ }
+ return false;
}
@Override
- public void onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic) {
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ for (AbstractBleProfile profile : mSupportedProfiles) {
+ if (profile.onCharacteristicChanged(gatt, characteristic)) {
+ return true;
+ }
+ }
+ return false;
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+ for (AbstractBleProfile profile : mSupportedProfiles) {
+ profile.onReadRemoteRssi(gatt, rssi, status);
+ }
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java
index 258199ad8..dc8da3f87 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java
@@ -59,7 +59,7 @@ public abstract class AbstractBTLEOperation
* Subclasses must implement this. When invoked, #prePerform() returned
* successfully.
* Note that subclasses HAVE TO call #operationFinished() when the entire
- * opreation is done (successful or not).
+ * operation is done (successful or not).
*
* @throws IOException
*/
@@ -67,7 +67,7 @@ public abstract class AbstractBTLEOperation
/**
* You MUST call this method when the operation has finished, either
- * successfull or unsuccessfully.
+ * successfully or unsuccessfully.
*
* @throws IOException
*/
@@ -105,8 +105,10 @@ public abstract class AbstractBTLEOperation
}
protected void unsetBusy() {
- getDevice().unsetBusyTask();
- getDevice().sendDeviceUpdateIntent(getContext());
+ if (getDevice().isBusy()) {
+ getDevice().unsetBusyTask();
+ getDevice().sendDeviceUpdateIntent(getContext());
+ }
}
public boolean isOperationRunning() {
@@ -133,28 +135,28 @@ public abstract class AbstractBTLEOperation
}
@Override
- public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
- mSupport.onCharacteristicRead(gatt, characteristic, status);
+ public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ return mSupport.onCharacteristicRead(gatt, characteristic, status);
}
@Override
- public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
- mSupport.onCharacteristicWrite(gatt, characteristic, status);
+ public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ return mSupport.onCharacteristicWrite(gatt, characteristic, status);
}
@Override
- public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
- mSupport.onCharacteristicChanged(gatt, characteristic);
+ public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ return mSupport.onCharacteristicChanged(gatt, characteristic);
}
@Override
- public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
- mSupport.onDescriptorRead(gatt, descriptor, status);
+ public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ return mSupport.onDescriptorRead(gatt, descriptor, status);
}
@Override
- public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
- mSupport.onDescriptorWrite(gatt, descriptor, status);
+ public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ return mSupport.onDescriptorWrite(gatt, descriptor, status);
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java
index 76f0a839e..5ef65460e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java
@@ -17,23 +17,28 @@ public abstract class AbstractGattCallback implements GattCallback {
}
@Override
- public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ return false;
}
@Override
- public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ return false;
}
@Override
- public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ return false;
}
@Override
- public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ return false;
}
@Override
- public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ return false;
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java
new file mode 100644
index 000000000..1bb4b88f0
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java
@@ -0,0 +1,218 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
+
+/**
+ * Provides methods to convert standard BLE units to byte sequences and vice versa.
+ */
+public class BLETypeConversions {
+ /**
+ * Converts a timestamp to the byte sequence to be sent to the current time characteristic
+ *
+ * @param timestamp
+ * @return
+ * @see GattCharacteristic#UUID_CHARACTERISTIC_CURRENT_TIME
+ */
+ public static byte[] calendarToRawBytes(Calendar timestamp, boolean honorDeviceTimeOffset) {
+
+ // The mi-band device currently records sleep
+ // only if it happens after 10pm and before 7am.
+ // The offset is used to trick the device to record sleep
+ // in non-standard hours.
+ // If you usually sleep, say, from 6am to 2pm, set the
+ // shift to -8, so at 6am the device thinks it's still 10pm
+ // of the day before.
+ if (honorDeviceTimeOffset) {
+ int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours();
+ if (offsetInHours != 0) {
+ timestamp.add(Calendar.HOUR_OF_DAY, offsetInHours);
+ }
+ }
+
+ // MiBand2:
+ // year,year,month,dayofmonth,hour,minute,second,dayofweek,0,0,tz
+
+ byte[] year = fromUint16(timestamp.get(Calendar.YEAR));
+ return new byte[] {
+ year[0],
+ year[1],
+ fromUint8(timestamp.get(Calendar.MONTH) + 1),
+ fromUint8(timestamp.get(Calendar.DATE)),
+ fromUint8(timestamp.get(Calendar.HOUR_OF_DAY)),
+ fromUint8(timestamp.get(Calendar.MINUTE)),
+ fromUint8(timestamp.get(Calendar.SECOND)),
+ dayOfWeekToRawBytes(timestamp),
+ 0, // fractions256 (not set)
+ // 0 (DST offset?) Mi2
+ // k (tz) Mi2
+ };
+ }
+
+ /**
+ * Similar to calendarToRawBytes, but only up to (and including) the MINUTES.
+ * @param timestamp
+ * @param honorDeviceTimeOffset
+ * @return
+ */
+ public static byte[] shortCalendarToRawBytes(Calendar timestamp, boolean honorDeviceTimeOffset) {
+
+ // The mi-band device currently records sleep
+ // only if it happens after 10pm and before 7am.
+ // The offset is used to trick the device to record sleep
+ // in non-standard hours.
+ // If you usually sleep, say, from 6am to 2pm, set the
+ // shift to -8, so at 6am the device thinks it's still 10pm
+ // of the day before.
+ if (honorDeviceTimeOffset) {
+ int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours();
+ if (offsetInHours != 0) {
+ timestamp.add(Calendar.HOUR_OF_DAY, offsetInHours);
+ }
+ }
+
+ // MiBand2:
+ // year,year,month,dayofmonth,hour,minute
+
+ byte[] year = fromUint16(timestamp.get(Calendar.YEAR));
+ return new byte[] {
+ year[0],
+ year[1],
+ fromUint8(timestamp.get(Calendar.MONTH) + 1),
+ fromUint8(timestamp.get(Calendar.DATE)),
+ fromUint8(timestamp.get(Calendar.HOUR_OF_DAY)),
+ fromUint8(timestamp.get(Calendar.MINUTE))
+ };
+ }
+
+ private static int getMiBand2TimeZone(int rawOffset) {
+ int offsetMinutes = rawOffset / 1000 / 60;
+ rawOffset = offsetMinutes < 0 ? -1 : 1;
+ offsetMinutes = Math.abs(offsetMinutes);
+ int offsetHours = offsetMinutes / 60;
+ rawOffset *= offsetMinutes % 60 / 15 + offsetHours * 4;
+ return rawOffset;
+ }
+
+ private static byte dayOfWeekToRawBytes(Calendar cal) {
+ int calValue = cal.get(Calendar.DAY_OF_WEEK);
+ switch (calValue) {
+ case Calendar.SUNDAY:
+ return 7;
+ default:
+ return (byte) (calValue - 1);
+ }
+ }
+
+ /**
+ * uses the standard algorithm to convert bytes received from the MiBand to a Calendar object
+ *
+ * @param value
+ * @return
+ */
+ public static GregorianCalendar rawBytesToCalendar(byte[] value, boolean honorDeviceTimeOffset) {
+ if (value.length >= 7) {
+ int year = toUint16(value[0], value[1]);
+ GregorianCalendar timestamp = new GregorianCalendar(
+ year,
+ (value[2] & 0xff) - 1,
+ value[3] & 0xff,
+ value[4] & 0xff,
+ value[5] & 0xff,
+ value[6] & 0xff
+ );
+
+ if (honorDeviceTimeOffset) {
+ int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours();
+ if (offsetInHours != 0) {
+ timestamp.add(Calendar.HOUR_OF_DAY,-offsetInHours);
+ }
+ }
+
+ return timestamp;
+ }
+
+ return createCalendar();
+ }
+
+ public static int toUint16(byte... bytes) {
+ return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8);
+ }
+
+ public static byte[] fromUint16(int value) {
+ return new byte[] {
+ (byte) (value & 0xff),
+ (byte) ((value >> 8) & 0xff),
+ };
+ }
+ public static byte fromUint8(int value) {
+ return (byte) (value & 0xff);
+ }
+
+ /**
+ * Creates a calendar object representing the current date and time.
+ */
+ public static GregorianCalendar createCalendar() {
+ return new GregorianCalendar();
+ }
+
+ public static byte[] join(byte[] start, byte[] end) {
+ if (start == null || start.length == 0) {
+ return end;
+ }
+ if (end == null || end.length == 0) {
+ return start;
+ }
+
+ byte[] result = new byte[start.length + end.length];
+ System.arraycopy(start, 0, result, 0, start.length);
+ System.arraycopy(end, 0, result, start.length, end.length);
+ return result;
+ }
+
+ public static byte[] calendarToLocalTimeBytes(GregorianCalendar now) {
+ byte[] result = new byte[2];
+ result[0] = mapTimeZone(now.getTimeZone());
+ result[1] = mapDstOffset(now);
+ return result;
+ }
+
+ /**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.time_zone.xml
+ * @param timeZone
+ * @return sint8 value from -48..+56
+ */
+ public static byte mapTimeZone(TimeZone timeZone) {
+ int utcOffsetInHours = (timeZone.getRawOffset() / (1000 * 60 * 60));
+ return (byte) (utcOffsetInHours * 4);
+ }
+
+ /**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.dst_offset.xml
+ * @param now
+ * @return the DST offset for the given time; 0 if none; 255 if unknown
+ */
+ public static byte mapDstOffset(Calendar now) {
+ TimeZone timeZone = now.getTimeZone();
+ int dstSavings = timeZone.getDSTSavings();
+ if (dstSavings == 0) {
+ return 0;
+ }
+ if (timeZone.inDaylightTime(now.getTime())) {
+ int dstInMinutes = dstSavings / (1000 * 60);
+ switch (dstInMinutes) {
+ case 30:
+ return 2;
+ case 60:
+ return 4;
+ case 120:
+ return 8;
+ }
+ return fromUint8(255); // unknown
+ }
+ return 0;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java
new file mode 100644
index 000000000..c48a362a5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java
@@ -0,0 +1,203 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle;
+
+import android.util.SparseArray;
+
+import java.util.HashMap;
+
+public class BleNamesResolver {
+ private static HashMap mServices = new HashMap();
+ private static HashMap mCharacteristics = new HashMap();
+ private static SparseArray mValueFormats = new SparseArray();
+ private static SparseArray mAppearance = new SparseArray();
+ private static SparseArray mHeartRateSensorLocation = new SparseArray();
+
+ static public String resolveServiceName(final String uuid)
+ {
+ String result = mServices.get(uuid);
+ if(result == null) result = "Unknown Service";
+ return result;
+ }
+
+ static public String resolveValueTypeDescription(final int format)
+ {
+ Integer tmp = Integer.valueOf(format);
+ return mValueFormats.get(tmp, "Unknown Format");
+ }
+
+ static public String resolveCharacteristicName(final String uuid)
+ {
+ String result = mCharacteristics.get(uuid);
+ if(result == null) result = "Unknown Characteristic";
+ return result;
+ }
+
+ static public String resolveUuid(final String uuid) {
+ String result = mServices.get(uuid);
+ if(result != null) return "Service: " + result;
+
+ result = mCharacteristics.get(uuid);
+ if(result != null) return "Characteristic: " + result;
+
+ result = "Unknown UUID";
+ return result;
+ }
+
+ static public String resolveAppearance(int key) {
+ Integer tmp = Integer.valueOf(key);
+ return mAppearance.get(tmp, "Unknown Appearance");
+ }
+
+ static public String resolveHeartRateSensorLocation(int key) {
+ Integer tmp = Integer.valueOf(key);
+ return mHeartRateSensorLocation.get(tmp, "Other");
+ }
+
+ static public boolean isService(final String uuid) {
+ return mServices.containsKey(uuid);
+ }
+
+ static public boolean isCharacteristic(final String uuid) {
+ return mCharacteristics.containsKey(uuid);
+ }
+
+ static {
+ mServices.put("00001811-0000-1000-8000-00805f9b34fb", "Alert Notification Service");
+ mServices.put("0000180f-0000-1000-8000-00805f9b34fb", "Battery Service");
+ mServices.put("00001810-0000-1000-8000-00805f9b34fb", "Blood Pressure");
+ mServices.put("00001805-0000-1000-8000-00805f9b34fb", "Current Time Service");
+ mServices.put("00001818-0000-1000-8000-00805f9b34fb", "Cycling Power");
+ mServices.put("00001816-0000-1000-8000-00805f9b34fb", "Cycling Speed and Cadence");
+ mServices.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information");
+ mServices.put("00001800-0000-1000-8000-00805f9b34fb", "Generic Access");
+ mServices.put("00001801-0000-1000-8000-00805f9b34fb", "Generic Attribute");
+ mServices.put("00001808-0000-1000-8000-00805f9b34fb", "Glucose");
+ mServices.put("00001809-0000-1000-8000-00805f9b34fb", "Health Thermometer");
+ mServices.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate");
+ mServices.put("00001812-0000-1000-8000-00805f9b34fb", "Human Interface Device");
+ mServices.put("00001802-0000-1000-8000-00805f9b34fb", "Immediate Alert");
+ mServices.put("00001803-0000-1000-8000-00805f9b34fb", "Link Loss");
+ mServices.put("00001819-0000-1000-8000-00805f9b34fb", "Location and Navigation");
+ mServices.put("00001807-0000-1000-8000-00805f9b34fb", "Next DST Change Service");
+ mServices.put("0000180e-0000-1000-8000-00805f9b34fb", "Phone Alert Status Service");
+ mServices.put("00001806-0000-1000-8000-00805f9b34fb", "Reference Time Update Service");
+ mServices.put("00001814-0000-1000-8000-00805f9b34fb", "Running Speed and Cadence");
+ mServices.put("00001813-0000-1000-8000-00805f9b34fb", "Scan Parameters");
+ mServices.put("00001804-0000-1000-8000-00805f9b34fb", "Tx Power");
+ mServices.put("0000fee0-0000-3512-2118-0009af100700", "(Propr: Xiaomi MiLi Service)");
+ mServices.put("00001530-0000-3512-2118-0009af100700", "(Propr: Xiaomi Weight Service)");
+
+
+ mCharacteristics.put("00002a43-0000-1000-8000-00805f9b34fb", "Alert AlertCategory ID");
+ mCharacteristics.put("00002a42-0000-1000-8000-00805f9b34fb", "Alert AlertCategory ID Bit Mask");
+ mCharacteristics.put("00002a06-0000-1000-8000-00805f9b34fb", "Alert Level");
+ mCharacteristics.put("00002a44-0000-1000-8000-00805f9b34fb", "Alert Notification Control Point");
+ mCharacteristics.put("00002a3f-0000-1000-8000-00805f9b34fb", "Alert Status");
+ mCharacteristics.put("00002a01-0000-1000-8000-00805f9b34fb", "Appearance");
+ mCharacteristics.put("00002a19-0000-1000-8000-00805f9b34fb", "Battery Level");
+ mCharacteristics.put("00002a49-0000-1000-8000-00805f9b34fb", "Blood Pressure Feature");
+ mCharacteristics.put("00002a35-0000-1000-8000-00805f9b34fb", "Blood Pressure Measurement");
+ mCharacteristics.put("00002a38-0000-1000-8000-00805f9b34fb", "Body Sensor Location");
+ mCharacteristics.put("00002a22-0000-1000-8000-00805f9b34fb", "Boot Keyboard Input Report");
+ mCharacteristics.put("00002a32-0000-1000-8000-00805f9b34fb", "Boot Keyboard Output Report");
+ mCharacteristics.put("00002a33-0000-1000-8000-00805f9b34fb", "Boot Mouse Input Report");
+ mCharacteristics.put("00002a5c-0000-1000-8000-00805f9b34fb", "CSC Feature");
+ mCharacteristics.put("00002a5b-0000-1000-8000-00805f9b34fb", "CSC Measurement");
+ mCharacteristics.put("00002a2b-0000-1000-8000-00805f9b34fb", "Current Time");
+ mCharacteristics.put("00002a66-0000-1000-8000-00805f9b34fb", "Cycling Power Control Point");
+ mCharacteristics.put("00002a65-0000-1000-8000-00805f9b34fb", "Cycling Power Feature");
+ mCharacteristics.put("00002a63-0000-1000-8000-00805f9b34fb", "Cycling Power Measurement");
+ mCharacteristics.put("00002a64-0000-1000-8000-00805f9b34fb", "Cycling Power Vector");
+ mCharacteristics.put("00002a08-0000-1000-8000-00805f9b34fb", "Date Time");
+ mCharacteristics.put("00002a0a-0000-1000-8000-00805f9b34fb", "Day Date Time");
+ mCharacteristics.put("00002a09-0000-1000-8000-00805f9b34fb", "Day of Week");
+ mCharacteristics.put("00002a00-0000-1000-8000-00805f9b34fb", "Device Name");
+ mCharacteristics.put("00002a0d-0000-1000-8000-00805f9b34fb", "DST Offset");
+ mCharacteristics.put("00002a0c-0000-1000-8000-00805f9b34fb", "Exact Time 256");
+ mCharacteristics.put("00002a26-0000-1000-8000-00805f9b34fb", "Firmware Revision String");
+ mCharacteristics.put("00002a51-0000-1000-8000-00805f9b34fb", "Glucose Feature");
+ mCharacteristics.put("00002a18-0000-1000-8000-00805f9b34fb", "Glucose Measurement");
+ mCharacteristics.put("00002a34-0000-1000-8000-00805f9b34fb", "Glucose Measurement Context");
+ mCharacteristics.put("00002a27-0000-1000-8000-00805f9b34fb", "Hardware Revision String");
+ mCharacteristics.put("00002a39-0000-1000-8000-00805f9b34fb", "Heart Rate Control Point");
+ mCharacteristics.put("00002a37-0000-1000-8000-00805f9b34fb", "Heart Rate Measurement");
+ mCharacteristics.put("00002a4c-0000-1000-8000-00805f9b34fb", "HID Control Point");
+ mCharacteristics.put("00002a4a-0000-1000-8000-00805f9b34fb", "HID Information");
+ mCharacteristics.put("00002a2a-0000-1000-8000-00805f9b34fb", "IEEE 11073-20601 Regulatory Certification Data List");
+ mCharacteristics.put("00002a36-0000-1000-8000-00805f9b34fb", "Intermediate Cuff Pressure");
+ mCharacteristics.put("00002a1e-0000-1000-8000-00805f9b34fb", "Intermediate Temperature");
+ mCharacteristics.put("00002a6b-0000-1000-8000-00805f9b34fb", "LN Control Point");
+ mCharacteristics.put("00002a6a-0000-1000-8000-00805f9b34fb", "LN Feature");
+ mCharacteristics.put("00002a0f-0000-1000-8000-00805f9b34fb", "Local Time Information");
+ mCharacteristics.put("00002a67-0000-1000-8000-00805f9b34fb", "Location and Speed");
+ mCharacteristics.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String");
+ mCharacteristics.put("00002a21-0000-1000-8000-00805f9b34fb", "Measurement Interval");
+ mCharacteristics.put("00002a24-0000-1000-8000-00805f9b34fb", "Model Number String");
+ mCharacteristics.put("00002a68-0000-1000-8000-00805f9b34fb", "Navigation");
+ mCharacteristics.put("00002a46-0000-1000-8000-00805f9b34fb", "New Alert");
+ mCharacteristics.put("00002a04-0000-1000-8000-00805f9b34fb", "Peripheral Preferred Connection Parameters");
+ mCharacteristics.put("00002a02-0000-1000-8000-00805f9b34fb", "Peripheral Privacy Flag");
+ mCharacteristics.put("00002a50-0000-1000-8000-00805f9b34fb", "PnP ID");
+ mCharacteristics.put("00002a69-0000-1000-8000-00805f9b34fb", "Position Quality");
+ mCharacteristics.put("00002a4e-0000-1000-8000-00805f9b34fb", "Protocol Mode");
+ mCharacteristics.put("00002a03-0000-1000-8000-00805f9b34fb", "Reconnection Address");
+ mCharacteristics.put("00002a52-0000-1000-8000-00805f9b34fb", "Record Access Control Point");
+ mCharacteristics.put("00002a14-0000-1000-8000-00805f9b34fb", "Reference Time Information");
+ mCharacteristics.put("00002a4d-0000-1000-8000-00805f9b34fb", "Report");
+ mCharacteristics.put("00002a4b-0000-1000-8000-00805f9b34fb", "Report Map");
+ mCharacteristics.put("00002a40-0000-1000-8000-00805f9b34fb", "Ringer Control Point");
+ mCharacteristics.put("00002a41-0000-1000-8000-00805f9b34fb", "Ringer Setting");
+ mCharacteristics.put("00002a54-0000-1000-8000-00805f9b34fb", "RSC Feature");
+ mCharacteristics.put("00002a53-0000-1000-8000-00805f9b34fb", "RSC Measurement");
+ mCharacteristics.put("00002a55-0000-1000-8000-00805f9b34fb", "SC Control Point");
+ mCharacteristics.put("00002a4f-0000-1000-8000-00805f9b34fb", "Scan Interval Window");
+ mCharacteristics.put("00002a31-0000-1000-8000-00805f9b34fb", "Scan Refresh");
+ mCharacteristics.put("00002a5d-0000-1000-8000-00805f9b34fb", "Sensor Location");
+ mCharacteristics.put("00002a25-0000-1000-8000-00805f9b34fb", "Serial Number String");
+ mCharacteristics.put("00002a05-0000-1000-8000-00805f9b34fb", "Service Changed");
+ mCharacteristics.put("00002a28-0000-1000-8000-00805f9b34fb", "Software Revision String");
+ mCharacteristics.put("00002a47-0000-1000-8000-00805f9b34fb", "Supported New Alert AlertCategory");
+ mCharacteristics.put("00002a48-0000-1000-8000-00805f9b34fb", "Supported Unread Alert AlertCategory");
+ mCharacteristics.put("00002a23-0000-1000-8000-00805f9b34fb", "System ID");
+ mCharacteristics.put("00002a1c-0000-1000-8000-00805f9b34fb", "Temperature Measurement");
+ mCharacteristics.put("00002a1d-0000-1000-8000-00805f9b34fb", "Temperature Type");
+ mCharacteristics.put("00002a12-0000-1000-8000-00805f9b34fb", "Time Accuracy");
+ mCharacteristics.put("00002a13-0000-1000-8000-00805f9b34fb", "Time Source");
+ mCharacteristics.put("00002a16-0000-1000-8000-00805f9b34fb", "Time Update Control Point");
+ mCharacteristics.put("00002a17-0000-1000-8000-00805f9b34fb", "Time Update State");
+ mCharacteristics.put("00002a11-0000-1000-8000-00805f9b34fb", "Time with DST");
+ mCharacteristics.put("00002a0e-0000-1000-8000-00805f9b34fb", "Time Zone");
+ mCharacteristics.put("00002a07-0000-1000-8000-00805f9b34fb", "Tx Power Level");
+ mCharacteristics.put("00002a45-0000-1000-8000-00805f9b34fb", "Unread Alert Status");
+
+ mValueFormats.put(Integer.valueOf(52), "32bit float");
+ mValueFormats.put(Integer.valueOf(50), "16bit float");
+ mValueFormats.put(Integer.valueOf(34), "16bit signed int");
+ mValueFormats.put(Integer.valueOf(36), "32bit signed int");
+ mValueFormats.put(Integer.valueOf(33), "8bit signed int");
+ mValueFormats.put(Integer.valueOf(18), "16bit unsigned int");
+ mValueFormats.put(Integer.valueOf(20), "32bit unsigned int");
+ mValueFormats.put(Integer.valueOf(17), "8bit unsigned int");
+
+ // lets add also couple appearance string description
+ // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml
+ mAppearance.put(Integer.valueOf(833), "Heart Rate Sensor: Belt");
+ mAppearance.put(Integer.valueOf(832), "Generic Heart Rate Sensor");
+ mAppearance.put(Integer.valueOf(0), "Unknown");
+ mAppearance.put(Integer.valueOf(64), "Generic Phone");
+ mAppearance.put(Integer.valueOf(1157), "Cycling: Speed and Cadence Sensor");
+ mAppearance.put(Integer.valueOf(1152), "General Cycling");
+ mAppearance.put(Integer.valueOf(1153), "Cycling Computer");
+ mAppearance.put(Integer.valueOf(1154), "Cycling: Speed Sensor");
+ mAppearance.put(Integer.valueOf(1155), "Cycling: Cadence Sensor");
+ mAppearance.put(Integer.valueOf(1156), "Cycling: Speed and Cadence Sensor");
+ mAppearance.put(Integer.valueOf(1157), "Cycling: Power Sensor");
+
+ mHeartRateSensorLocation.put(Integer.valueOf(0), "Other");
+ mHeartRateSensorLocation.put(Integer.valueOf(1), "Chest");
+ mHeartRateSensorLocation.put(Integer.valueOf(2), "Wrist");
+ mHeartRateSensorLocation.put(Integer.valueOf(3), "Finger");
+ mHeartRateSensorLocation.put(Integer.valueOf(4), "Hand");
+ mHeartRateSensorLocation.put(Integer.valueOf(5), "Ear Lobe");
+ mHeartRateSensorLocation.put(Integer.valueOf(6), "Foot");
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java
index faf35aa6b..db2587c65 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java
@@ -25,7 +25,7 @@ public abstract class BtLEAction {
}
/**
- * Returns true if this actions expects an (async) result which must
+ * Returns true if this action expects an (async) result which must
* be waited for, before continuing with other actions.
*
* This is needed because the current Bluedroid stack can only deal
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java
index fc7358a81..f81285d9f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java
@@ -14,6 +14,7 @@ import android.support.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
@@ -47,7 +48,7 @@ public final class BtLEQueue {
private final InternalGattCallback internalGattCallback;
private boolean mAutoReconnect;
- private Thread dispatchThread = new Thread("GadgetBridge GATT Dispatcher") {
+ private Thread dispatchThread = new Thread("Gadgetbridge GATT Dispatcher") {
@Override
public void run() {
@@ -147,7 +148,7 @@ public final class BtLEQueue {
}
synchronized (mGattMonitor) {
if (mBluetoothGatt != null) {
- // Tribal knowledge says you're better off not reusing existing BlueoothGatt connections,
+ // Tribal knowledge says you're better off not reusing existing BluetoothGatt connections,
// so create a new one.
LOG.info("connect() requested -- disconnecting previous connection: " + mGbDevice.getName());
disconnect();
@@ -198,6 +199,7 @@ public final class BtLEQueue {
if (mWaitForActionResultLatch != null) {
mWaitForActionResultLatch.countDown();
}
+ boolean wasInitialized = mGbDevice.isInitialized();
setDeviceConnectionState(State.NOT_CONNECTED);
// either we've been disconnected because the device is out of range
@@ -207,7 +209,7 @@ public final class BtLEQueue {
// reconnecting automatically, so we try to fix this by re-creating mBluetoothGatt.
// Not sure if this actually works without re-initializing the device...
if (status != 0) {
- if (!maybeReconnect()) {
+ if (!wasInitialized || !maybeReconnect()) {
disconnect(); // ensure that we start over cleanly next time
}
}
@@ -257,6 +259,23 @@ public final class BtLEQueue {
}
}
+ /**
+ * Adds a transaction to the beginning of the queue.
+ * Note that actions of the *currently executing* transaction
+ * will still be executed before the given transaction.
+ *
+ * @param transaction
+ */
+ public void insert(Transaction transaction) {
+ LOG.debug("about to insert: " + transaction);
+ if (!transaction.isEmpty()) {
+ List tail = new ArrayList<>(mTransactions.size() + 2);
+ mTransactions.drainTo(tail);
+ mTransactions.add(transaction);
+ mTransactions.addAll(tail);
+ }
+ }
+
public void clear() {
mTransactions.clear();
}
@@ -329,8 +348,14 @@ public final class BtLEQueue {
LOG.info("Connected to GATT server.");
setDeviceConnectionState(State.CONNECTED);
// Attempts to discover services after successful connection.
- LOG.info("Attempting to start service discovery:" +
- gatt.discoverServices());
+ List cachedServices = gatt.getServices();
+ if (cachedServices != null && cachedServices.size() > 0) {
+ LOG.info("Using cached services, skipping discovery");
+ onServicesDiscovered(gatt, BluetoothGatt.GATT_SUCCESS);
+ } else {
+ LOG.info("Attempting to start service discovery:" +
+ gatt.discoverServices());
+ }
break;
case BluetoothProfile.STATE_DISCONNECTED:
LOG.info("Disconnected from GATT server.");
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java
index 3089856b4..107af38da 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java
@@ -25,6 +25,10 @@ import android.bluetooth.BluetoothGattDescriptor;
* Callback interface handling gatt events.
* Pretty much the same as {@link BluetoothGattCallback}, except it's an interface
* instead of an abstract class. Some handlers commented out, because not used (yet).
+ *
+ * Note: the boolean return values indicate whether this callback "consumed" this event
+ * or not. True means, the event was consumed by this instance and no further instances
+ * shall be notified. Fallse means, this instance could not handle the event.
*/
public interface GattCallback {
@@ -48,7 +52,7 @@ public interface GattCallback {
* @param status
* @see BluetoothGattCallback#onCharacteristicRead(BluetoothGatt, BluetoothGattCharacteristic, int)
*/
- void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status);
+ boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status);
/**
* @param gatt
@@ -56,7 +60,7 @@ public interface GattCallback {
* @param status
* @see BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)
*/
- void onCharacteristicWrite(BluetoothGatt gatt,
+ boolean onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status);
/**
@@ -64,7 +68,7 @@ public interface GattCallback {
* @param characteristic
* @see BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic)
*/
- void onCharacteristicChanged(BluetoothGatt gatt,
+ boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic);
/**
@@ -73,7 +77,7 @@ public interface GattCallback {
* @param status
* @see BluetoothGattCallback#onDescriptorRead(BluetoothGatt, BluetoothGattDescriptor, int)
*/
- void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status);
/**
@@ -82,7 +86,7 @@ public interface GattCallback {
* @param status
* @see BluetoothGattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int)
*/
- void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status);
//
// /**
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java
index afbd9ee6d..812454a48 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java
@@ -1,5 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.btle;
+import android.bluetooth.BluetoothGattCharacteristic;
+
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@@ -182,8 +184,8 @@ public class GattCharacteristic {
static {
GATTCHARACTERISTIC_DEBUG = new HashMap<>();
- GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID, "Alert Category ID");
- GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID_BIT_MASK, "Alert Category ID Bit Mask");
+ GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID, "Alert AlertCategory ID");
+ GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID_BIT_MASK, "Alert AlertCategory ID Bit Mask");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_LEVEL, "Alert Level");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_NOTIFICATION_CONTROL_POINT, "Alert Notification Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_STATUS, "Alert Status");
@@ -219,8 +221,8 @@ public class GattCharacteristic {
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING, "Serial Number String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GATT_SERVICE_CHANGED, "Service Changed");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING, "Software Revision String");
- GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_NEW_ALERT_CATEGORY, "Supported New Alert Category");
- GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_UNREAD_ALERT_CATEGORY, "Supported Unread Alert Category");
+ GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_NEW_ALERT_CATEGORY, "Supported New Alert AlertCategory");
+ GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_UNREAD_ALERT_CATEGORY, "Supported Unread Alert AlertCategory");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SYSTEM_ID, "System ID");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT, "Temperature Measurement");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TEMPERATURE_TYPE, "Temperature DeviceType");
@@ -241,4 +243,7 @@ public class GattCharacteristic {
return name;
}
+ public static String toString(BluetoothGattCharacteristic characteristic) {
+ return characteristic.getUuid() + " (" + lookup(characteristic.getUuid(), "unknown") + ")";
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java
index 797730316..4e5b4eb99 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java
@@ -9,7 +9,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
-import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION;
@@ -20,7 +19,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.U
*/
public class NotifyAction extends BtLEAction {
- private static final Logger LOG = LoggerFactory.getLogger(TransactionBuilder.class);
+ private static final Logger LOG = LoggerFactory.getLogger(NotifyAction.class);
protected final boolean enableFlag;
private boolean hasWrittenDescriptor = true;
@@ -49,7 +48,7 @@ public class NotifyAction extends BtLEAction {
hasWrittenDescriptor = false;
}
} else {
- LOG.warn("sleep descriptor null");
+ LOG.warn("Descriptor CLIENT_CHARACTERISTIC_CONFIGURATION for characteristic " + getCharacteristic().getUuid() + " is null");
hasWrittenDescriptor = false;
}
} else {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java
index 5cecee06d..95e765f92 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java
@@ -4,6 +4,10 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
/**
@@ -12,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
* {@link BluetoothGattCallback}
*/
public class WriteAction extends BtLEAction {
+ private static final Logger LOG = LoggerFactory.getLogger(WriteAction.class);
private final byte[] value;
@@ -24,7 +29,7 @@ public class WriteAction extends BtLEAction {
public boolean run(BluetoothGatt gatt) {
BluetoothGattCharacteristic characteristic = getCharacteristic();
int properties = characteristic.getProperties();
- //TODO: expectsResult should return false if PROPERTY_WRITE_NO_RESPONSE is true, but this yelds to timing issues
+ //TODO: expectsResult should return false if PROPERTY_WRITE_NO_RESPONSE is true, but this leads to timing issues
if ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 || ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0)) {
return writeValue(gatt, characteristic, value);
}
@@ -32,6 +37,9 @@ public class WriteAction extends BtLEAction {
}
protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("writing to characteristic: " + characteristic.getUuid() + ": " + Logging.formatBytes(value));
+ }
if (characteristic.setValue(value)) {
return gatt.writeCharacteristic(characteristic);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java
new file mode 100644
index 000000000..50122b41f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java
@@ -0,0 +1,73 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.content.LocalBroadcastManager;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractGattCallback;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+
+/**
+ * Base class for all BLE profiles, with things that all impplementations are
+ * expected to use.
+ *
+ * Instances are used in the context of a concrete AbstractBTLEDeviceSupport instance,
+ * i.e. a concrete device.
+ *
+ * @see nodomain.freeyourgadget.gadgetbridge.service.btle.GattService
+ * @see nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic
+ * @see https://www.bluetooth.com/specifications/assigned-numbers
+ */
+public abstract class AbstractBleProfile extends AbstractGattCallback {
+ private final T mSupport;
+
+ public AbstractBleProfile(T support) {
+ this.mSupport = support;
+ }
+
+ /**
+ * All notifications should be sent through this methods to make them testable.
+ * @param intent the intent to broadcast
+ */
+ protected void notify(Intent intent) {
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ }
+
+ /**
+ * Delegates to the DeviceSupport instance and additionally sets this instance as the Gatt
+ * callback for the transaction.
+ *
+ * @param taskName
+ * @return
+ * @throws IOException
+ */
+ public TransactionBuilder performInitialized(String taskName) throws IOException {
+ TransactionBuilder builder = mSupport.performInitialized(taskName);
+ builder.setGattCallback(this);
+ return builder;
+ }
+
+ public Context getContext() {
+ return mSupport.getContext();
+ }
+
+ protected GBDevice getDevice() {
+ return mSupport.getDevice();
+ }
+
+ protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
+ return mSupport.getCharacteristic(uuid);
+ }
+
+ protected BtLEQueue getQueue() {
+ return mSupport.getQueue();
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java
new file mode 100644
index 000000000..1e7d2bca3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java
@@ -0,0 +1,21 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
+
+public class ValueDecoder {
+ private static final Logger LOG = LoggerFactory.getLogger(ValueDecoder.class);
+
+ public static int decodePercent(BluetoothGattCharacteristic characteristic) {
+ int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
+ if (percent > 100 || percent < 0) {
+ LOG.warn("Unexpected percent value: " + percent + ": " + GattCharacteristic.toString(characteristic));
+ percent = Math.max(100, Math.min(0, percent));
+ }
+ return percent;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertCategory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertCategory.java
new file mode 100644
index 000000000..393a34db9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertCategory.java
@@ -0,0 +1,79 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id.xml
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id_bit_mask.xml
+ */
+public enum AlertCategory {
+ Simple(0),
+ Email(1),
+ News(2),
+ IncomingCall(3),
+ MissedCall(4),
+ SMS(5),
+ VoiceMail(6),
+ Schedule(7),
+ HighPriorityAlert(8),
+ InstantMessage(9);
+ // 10-250 reserved for future use
+ // 251-255 defined by service specification
+
+ private final int id;
+
+ AlertCategory(int id) {
+ this.id = id;
+ }
+
+ /**
+ * Returns the numerical ID value of this category
+ * To be used as uint8 value
+ * @return the uint8 value for this category
+ */
+ public int getId() {
+ return id;
+ }
+
+ private int realBitNumber() {
+ // the ID corresponds to the bit for the bitset
+ return id;
+ }
+
+ private int bitNumberPerByte() {
+ // the ID corresponds to the bit for the bitset (per byte)
+ return realBitNumber() % 8;
+ }
+
+ private int asBit() {
+ return 1 << bitNumberPerByte();
+ }
+
+ private int byteNumber() {
+ return id <= 7 ? 0 : 1;
+ }
+
+ /**
+ * Converts the given categories to an array of bytes.
+ * @param categories
+ * @return
+ */
+ public static byte[] toBitmask(AlertCategory... categories) {
+ byte[] result = new byte[2];
+
+ for (AlertCategory category : categories) {
+ result[category.byteNumber()] |= category.asBit();
+ }
+ return result;
+ }
+
+// SupportedNewAlertCategory
+ public static AlertCategory[] fromBitMask(byte[] bytes) {
+ List result = new ArrayList<>();
+ byte b = bytes[0];
+
+ return null;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertLevel.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertLevel.java
new file mode 100644
index 000000000..dd6f22f1f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertLevel.java
@@ -0,0 +1,27 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
+
+
+/**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_level.xml
+ */
+public enum AlertLevel {
+ NoAlert(0),
+ MildAlert(1),
+ HighAlert(2);
+ // 3-255 reserved
+
+ private final int id;
+
+ AlertLevel(int id) {
+ this.id = id;
+ }
+
+ /**
+ * The alert level ID
+ * To be used as uint8 value
+ * @return
+ */
+ public int getId() {
+ return id;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationControl.java
new file mode 100644
index 000000000..48c88a9c4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationControl.java
@@ -0,0 +1,8 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
+
+/**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_notification_control_point.xml
+ */
+public class AlertNotificationControl {
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationProfile.java
new file mode 100644
index 000000000..1f37e42ae
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationProfile.java
@@ -0,0 +1,12 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
+
+public class AlertNotificationProfile extends AbstractBleProfile {
+ public AlertNotificationProfile(T support) {
+ super(support);
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertStatus.java
new file mode 100644
index 000000000..38f2dd356
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertStatus.java
@@ -0,0 +1,21 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
+
+/**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_status.xml
+ * uint8 value (bitmask) of the given values
+ */
+public class AlertStatus {
+ public static final int RINGER_ACTIVE_BIT = 1;
+ public static final int VIBRATE_ACTIVE = 1 << 1;
+ public static final int DISPLAY_ALERT_ACTIVE = 1 << 2;
+
+ public static boolean isRingerActive(int status) {
+ return (status & RINGER_ACTIVE_BIT) == RINGER_ACTIVE_BIT;
+ }
+ public static boolean isVibrateActive(int status) {
+ return (status & VIBRATE_ACTIVE) == VIBRATE_ACTIVE;
+ }
+ public static boolean isDisplayAlertActive(int status) {
+ return (status & DISPLAY_ALERT_ACTIVE) == DISPLAY_ALERT_ACTIVE;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/SupportedNewAlertCategory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/SupportedNewAlertCategory.java
new file mode 100644
index 000000000..325d6daf8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/SupportedNewAlertCategory.java
@@ -0,0 +1,58 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
+
+/**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id.xml
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id_bit_mask.xml
+ */
+public class SupportedNewAlertCategory {
+ private final int id;
+//
+// public static Ca(byte[] categoryBytes) {
+//
+// }
+
+ public SupportedNewAlertCategory(int id) {
+ this.id = id;
+ }
+
+ /**
+ * Returns the numerical ID value of this category
+ * To be used as uint8 value
+ * @return the uint8 value for this category
+ */
+ public int getId() {
+ return id;
+ }
+
+ private int realBitNumber() {
+ // the ID corresponds to the bit for the bitset
+ return id;
+ }
+
+ private int bitNumberPerByte() {
+ // the ID corresponds to the bit for the bitset (per byte)
+ return realBitNumber() % 8;
+ }
+
+ private int asBit() {
+ return 1 << bitNumberPerByte();
+ }
+
+ private int byteNumber() {
+ return id <= 7 ? 0 : 1;
+ }
+
+ /**
+ * Converts the given categories to an array of bytes.
+ * @param categories
+ * @return
+ */
+ public static byte[] toBitmask(SupportedNewAlertCategory... categories) {
+ byte[] result = new byte[2];
+
+ for (SupportedNewAlertCategory category : categories) {
+ result[category.byteNumber()] |= category.asBit();
+ }
+ return result;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java
new file mode 100644
index 000000000..f13c47bf4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java
@@ -0,0 +1,46 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class BatteryInfo implements Parcelable{
+
+ private int percentCharged;
+
+ public BatteryInfo() {
+ }
+
+ protected BatteryInfo(Parcel in) {
+ percentCharged = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(percentCharged);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public BatteryInfo createFromParcel(Parcel in) {
+ return new BatteryInfo(in);
+ }
+
+ @Override
+ public BatteryInfo[] newArray(int size) {
+ return new BatteryInfo[size];
+ }
+ };
+
+ public int getPercentCharged() {
+ return percentCharged;
+ }
+
+ public void setPercentCharged(int percentCharged) {
+ this.percentCharged = percentCharged;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java
new file mode 100644
index 000000000..160c83eff
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java
@@ -0,0 +1,72 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.content.Intent;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.ValueDecoder;
+
+public class BatteryInfoProfile extends AbstractBleProfile {
+ private static final Logger LOG = LoggerFactory.getLogger(BatteryInfoProfile.class);
+
+ private static final String ACTION_PREFIX = BatteryInfoProfile.class.getName() + "_";
+
+ public static final String ACTION_BATTERY_INFO = ACTION_PREFIX + "BATTERY_INFO";
+ public static final String EXTRA_BATTERY_INFO = "BATTERY_INFO";
+
+ public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_BATTERY_SERVICE;
+
+ public static final UUID UUID_CHARACTERISTIC_BATTERY_LEVEL = GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL;
+ private final BatteryInfo batteryInfo = new BatteryInfo();
+
+ public BatteryInfoProfile(T support) {
+ super(support);
+ }
+
+ public void requestBatteryInfo(TransactionBuilder builder) {
+ builder.read(getCharacteristic(UUID_CHARACTERISTIC_BATTERY_LEVEL));
+ }
+
+ public void enableNotifiy() {
+ // TODO: notification
+ }
+
+ @Override
+ public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ UUID charUuid = characteristic.getUuid();
+ if (charUuid.equals(UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
+ handleBatteryLevel(gatt, characteristic);
+ return true;
+ } else {
+ LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic));
+ }
+ } else {
+ LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic));
+ }
+ return false;
+ }
+
+ private void handleBatteryLevel(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ int percent = ValueDecoder.decodePercent(characteristic);
+ batteryInfo.setPercentCharged(percent);
+
+ notify(createIntent(batteryInfo));
+ }
+
+ private Intent createIntent(BatteryInfo batteryInfo) {
+ Intent intent = new Intent(ACTION_BATTERY_INFO);
+ intent.putExtra(EXTRA_BATTERY_INFO, batteryInfo);
+ return intent;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java
new file mode 100644
index 000000000..de83b567b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java
@@ -0,0 +1,148 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class DeviceInfo implements Parcelable{
+ private String manufacturerName;
+ private String modelNumber;
+ private String serialNumber;
+ private String hardwareRevision;
+ private String firmwareRevision;
+ private String softwareRevision;
+ private String systemId;
+ private String regulatoryCertificationDataList;
+ private String pnpId;
+
+ public DeviceInfo() {
+ }
+
+ protected DeviceInfo(Parcel in) {
+ manufacturerName = in.readString();
+ modelNumber = in.readString();
+ serialNumber = in.readString();
+ hardwareRevision = in.readString();
+ firmwareRevision = in.readString();
+ softwareRevision = in.readString();
+ systemId = in.readString();
+ regulatoryCertificationDataList = in.readString();
+ pnpId = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(manufacturerName);
+ dest.writeString(modelNumber);
+ dest.writeString(serialNumber);
+ dest.writeString(hardwareRevision);
+ dest.writeString(firmwareRevision);
+ dest.writeString(softwareRevision);
+ dest.writeString(systemId);
+ dest.writeString(regulatoryCertificationDataList);
+ dest.writeString(pnpId);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public DeviceInfo createFromParcel(Parcel in) {
+ return new DeviceInfo(in);
+ }
+
+ @Override
+ public DeviceInfo[] newArray(int size) {
+ return new DeviceInfo[size];
+ }
+ };
+
+ public String getManufacturerName() {
+ return manufacturerName;
+ }
+
+ public void setManufacturerName(String manufacturerName) {
+ this.manufacturerName = manufacturerName;
+ }
+
+ public String getModelNumber() {
+ return modelNumber;
+ }
+
+ public void setModelNumber(String modelNumber) {
+ this.modelNumber = modelNumber;
+ }
+
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ public void setSerialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ }
+
+ public String getHardwareRevision() {
+ return hardwareRevision;
+ }
+
+ public void setHardwareRevision(String hardwareRevision) {
+ this.hardwareRevision = hardwareRevision;
+ }
+
+ public String getFirmwareRevision() {
+ return firmwareRevision;
+ }
+
+ public void setFirmwareRevision(String firmwareRevision) {
+ this.firmwareRevision = firmwareRevision;
+ }
+
+ public String getSoftwareRevision() {
+ return softwareRevision;
+ }
+
+ public void setSoftwareRevision(String softwareRevision) {
+ this.softwareRevision = softwareRevision;
+ }
+
+ public String getSystemId() {
+ return systemId;
+ }
+
+ public void setSystemId(String systemId) {
+ this.systemId = systemId;
+ }
+
+ public String getRegulatoryCertificationDataList() {
+ return regulatoryCertificationDataList;
+ }
+
+ public void setRegulatoryCertificationDataList(String regulatoryCertificationDataList) {
+ this.regulatoryCertificationDataList = regulatoryCertificationDataList;
+ }
+
+ public String getPnpId() {
+ return pnpId;
+ }
+
+ public void setPnpId(String pnpId) {
+ this.pnpId = pnpId;
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceInfo{" +
+ "manufacturerName='" + manufacturerName + '\'' +
+ ", modelNumber='" + modelNumber + '\'' +
+ ", serialNumber='" + serialNumber + '\'' +
+ ", hardwareRevision='" + hardwareRevision + '\'' +
+ ", firmwareRevision='" + firmwareRevision + '\'' +
+ ", softwareRevision='" + softwareRevision + '\'' +
+ ", systemId='" + systemId + '\'' +
+ ", regulatoryCertificationDataList='" + regulatoryCertificationDataList + '\'' +
+ ", pnpId='" + pnpId + '\'' +
+ '}';
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java
new file mode 100644
index 000000000..b312249fb
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java
@@ -0,0 +1,162 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.content.Intent;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
+
+public class DeviceInfoProfile extends AbstractBleProfile {
+ private static final Logger LOG = LoggerFactory.getLogger(DeviceInfoProfile.class);
+
+ private static final String ACTION_PREFIX = DeviceInfoProfile.class.getName() + "_";
+
+ public static final String ACTION_DEVICE_INFO = ACTION_PREFIX + "DEVICE_INFO";
+ public static final String EXTRA_DEVICE_INFO = "DEVICE_INFO";
+
+ public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_DEVICE_INFORMATION;
+
+ public static final UUID UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING = GattCharacteristic.UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING;
+ public static final UUID UUID_CHARACTERISTIC_MODEL_NUMBER_STRING = GattCharacteristic.UUID_CHARACTERISTIC_MODEL_NUMBER_STRING;
+ public static final UUID UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING = GattCharacteristic.UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING;
+ public static final UUID UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING;
+ public static final UUID UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING;
+ public static final UUID UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING;
+ public static final UUID UUID_CHARACTERISTIC_SYSTEM_ID = GattCharacteristic.UUID_CHARACTERISTIC_SYSTEM_ID;
+ public static final UUID UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST = GattCharacteristic.UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST;
+ public static final UUID UUID_CHARACTERISTIC_PNP_ID = GattCharacteristic.UUID_CHARACTERISTIC_PNP_ID;
+ private final DeviceInfo deviceInfo = new DeviceInfo();
+
+ public DeviceInfoProfile(T support) {
+ super(support);
+ }
+
+ public void requestDeviceInfo(TransactionBuilder builder) {
+ builder.read(getCharacteristic(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_SYSTEM_ID))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST))
+ .read(getCharacteristic(UUID_CHARACTERISTIC_PNP_ID));
+ }
+
+ @Override
+ public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ UUID charUuid = characteristic.getUuid();
+ if (charUuid.equals(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING)) {
+ handleManufacturerName(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING)) {
+ handleModelNumber(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING)) {
+ handleSerialNumber(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING)) {
+ handleHardwareRevision(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING)) {
+ handleFirmwareRevision(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING)) {
+ handleSoftwareRevision(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_SYSTEM_ID)) {
+ handleSystemId(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST)) {
+ handleRegulatoryCertificationData(gatt, characteristic);
+ return true;
+ } else if (charUuid.equals(UUID_CHARACTERISTIC_PNP_ID)) {
+ handlePnpId(gatt, characteristic);
+ return true;
+ } else {
+ LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic));
+ }
+ } else {
+ LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic));
+ }
+ return false;
+ }
+
+
+ private void handleManufacturerName(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ String name = characteristic.getStringValue(0);
+ deviceInfo.setManufacturerName(name);
+ notify(createIntent(deviceInfo));
+ }
+
+ private void handleModelNumber(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ String modelNumber = characteristic.getStringValue(0);
+ deviceInfo.setModelNumber(modelNumber);
+ notify(createIntent(deviceInfo));
+ }
+ private void handleSerialNumber(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ String serialNumber = characteristic.getStringValue(0);
+ deviceInfo.setSerialNumber(serialNumber);
+ notify(createIntent(deviceInfo));
+ }
+
+ private void handleHardwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ String hardwareRevision = characteristic.getStringValue(0);
+ deviceInfo.setHardwareRevision(hardwareRevision);
+ notify(createIntent(deviceInfo));
+ }
+
+ private void handleFirmwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ String firmwareRevision = characteristic.getStringValue(0);
+ deviceInfo.setFirmwareRevision(firmwareRevision);
+ notify(createIntent(deviceInfo));
+ }
+
+ private void handleSoftwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ String softwareRevision = characteristic.getStringValue(0);
+ deviceInfo.setSoftwareRevision(softwareRevision);
+ notify(createIntent(deviceInfo));
+ }
+
+ private void handleSystemId(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ String systemId = characteristic.getStringValue(0);
+ deviceInfo.setSystemId(systemId);
+ notify(createIntent(deviceInfo));
+ }
+
+ private void handleRegulatoryCertificationData(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ // TODO: regulatory certification data list not supported yet
+// String regulatoryCertificationData = characteristic.getStringValue(0);
+// deviceInfo.setRegulatoryCertificationDataList(regulatoryCertificationData);
+// notify(createIntent(deviceInfo));
+ }
+
+ private void handlePnpId(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ byte[] value = characteristic.getValue();
+ if (value.length == 7) {
+// int vendorSource
+//
+// deviceInfo.setPnpId(pnpId);
+ notify(createIntent(deviceInfo));
+ } else {
+ // TODO: LOG warning
+ }
+ }
+
+ private Intent createIntent(DeviceInfo deviceInfo) {
+ Intent intent = new Intent(ACTION_DEVICE_INFO);
+ intent.putExtra(EXTRA_DEVICE_INFO, deviceInfo); // TODO: broadcast a clone of the info
+ return intent;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/BodySensorLocation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/BodySensorLocation.java
new file mode 100644
index 000000000..4b71c5073
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/BodySensorLocation.java
@@ -0,0 +1,23 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate;
+
+/**
+ * The Body Sensor Location characteristic of the device is used to describe the intended location of the heart rate measurement for the device.
+ *
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml
+ */
+public enum BodySensorLocation {
+ Other(0),
+ Chest(1),
+ Wrist(2),
+ Finger(3),
+ Hand(4),
+ EarLobe(5),
+ Foot(6);
+ // others are reserved
+
+ private final int val;
+
+ BodySensorLocation(int val) {
+ this.val = val;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java
new file mode 100644
index 000000000..ec195fcec
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java
@@ -0,0 +1,62 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+/**
+ * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml
+ */
+public class HeartRateProfile extends AbstractBleProfile {
+ private static final Logger LOG = LoggerFactory.getLogger(HeartRateProfile.class);
+
+ /**
+ * Returned when a request to the heart rate control point is not supported by the device
+ */
+ public static final int ERR_CONTROL_POINT_NOT_SUPPORTED = 0x80;
+
+ public HeartRateProfile(T support) {
+ super(support);
+ }
+
+ public void resetEnergyExpended(TransactionBuilder builder) {
+ writeToControlPoint((byte) 0x01, builder);
+ }
+
+ protected void writeToControlPoint(byte value, TransactionBuilder builder) {
+ writeToControlPoint(new byte[] { value }, builder);
+ }
+
+ protected void writeToControlPoint(byte[] value, TransactionBuilder builder) {
+ builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), value);
+ }
+
+ public void requestBodySensorLocation(TransactionBuilder builder) {
+
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
+ int flag = characteristic.getProperties();
+ int format = -1;
+ if ((flag & 0x01) != 0) {
+ format = BluetoothGattCharacteristic.FORMAT_UINT16;
+ } else {
+ format = BluetoothGattCharacteristic.FORMAT_UINT8;
+ }
+ final int heartRate = characteristic.getIntValue(format, 1);
+ LOG.info("Heart rate: " + heartRate, Toast.LENGTH_LONG, GB.INFO);
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewIoThread.java
new file mode 100644
index 000000000..2034a07fa
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewIoThread.java
@@ -0,0 +1,220 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.os.ParcelUuid;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class LiveviewIoThread extends GBDeviceIoThread {
+ private static final Logger LOG = LoggerFactory.getLogger(LiveviewIoThread.class);
+
+ private static final UUID SERIAL = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
+
+ private final LiveviewProtocol mLiveviewProtocol;
+ private final LiveviewSupport mLiveviewSupport;
+
+
+ private BluetoothAdapter mBtAdapter = null;
+ private BluetoothSocket mBtSocket = null;
+ private InputStream mInStream = null;
+ private OutputStream mOutStream = null;
+ private boolean mQuit = false;
+
+ @Override
+ public void quit() {
+ mQuit = true;
+ if (mBtSocket != null) {
+ try {
+ mBtSocket.close();
+ } catch (IOException e) {
+ LOG.error(e.getMessage());
+ }
+ }
+ }
+
+ private boolean mIsConnected = false;
+
+
+ public LiveviewIoThread(GBDevice gbDevice, Context context, GBDeviceProtocol lvProtocol, LiveviewSupport lvSupport, BluetoothAdapter lvBtAdapter) {
+ super(gbDevice, context);
+ mLiveviewProtocol = (LiveviewProtocol) lvProtocol;
+ mBtAdapter = lvBtAdapter;
+ mLiveviewSupport = lvSupport;
+ }
+
+ @Override
+ public synchronized void write(byte[] bytes) {
+ if (null == bytes)
+ return;
+ LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length));
+ try {
+ mOutStream.write(bytes);
+ mOutStream.flush();
+ } catch (IOException e) {
+ LOG.error("Error writing.", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ mIsConnected = connect();
+ if (!mIsConnected) {
+ setUpdateState(GBDevice.State.NOT_CONNECTED);
+ return;
+ }
+ mQuit = false;
+
+ while (!mQuit) {
+ LOG.info("Ready for a new message exchange.");
+
+ try {
+ GBDeviceEvent deviceEvents[] = mLiveviewProtocol.decodeResponse(parseIncoming());
+ if (deviceEvents == null) {
+ LOG.info("unhandled message");
+ } else {
+ for (GBDeviceEvent deviceEvent : deviceEvents) {
+ if (deviceEvent == null) {
+ continue;
+ }
+ mLiveviewSupport.evaluateGBDeviceEvent(deviceEvent);
+ }
+ }
+ } catch (SocketTimeoutException ignore) {
+ LOG.debug("socket timeout, we can't help but ignore this");
+ } catch (IOException e) {
+ LOG.info(e.getMessage());
+ mIsConnected = false;
+ mBtSocket = null;
+ mInStream = null;
+ mOutStream = null;
+ LOG.info("Bluetooth socket closed, will quit IO Thread");
+ break;
+ }
+ }
+
+ mIsConnected = false;
+ if (mBtSocket != null) {
+ try {
+ mBtSocket.close();
+ } catch (IOException e) {
+ LOG.error(e.getMessage());
+ }
+ mBtSocket = null;
+ }
+ setUpdateState(GBDevice.State.NOT_CONNECTED);
+ }
+
+ @Override
+ protected boolean connect() {
+ GBDevice.State originalState = gbDevice.getState();
+ setUpdateState(GBDevice.State.CONNECTING);
+
+ try {
+ BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(gbDevice.getAddress());
+ ParcelUuid uuids[] = btDevice.getUuids();
+ if (uuids == null) {
+ return false;
+ }
+ for (ParcelUuid uuid : uuids) {
+ LOG.info("found service UUID " + uuid);
+ }
+ mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid());
+ mBtSocket.connect();
+ mInStream = mBtSocket.getInputStream();
+ mOutStream = mBtSocket.getOutputStream();
+ setUpdateState(GBDevice.State.CONNECTED);
+ } catch (IOException e) {
+ LOG.error("Server socket cannot be started.");
+ //LOG.error(e.getMessage());
+ setUpdateState(originalState);
+ mInStream = null;
+ mOutStream = null;
+ mBtSocket = null;
+ return false;
+ }
+
+ write(mLiveviewProtocol.encodeSetTime());
+ setUpdateState(GBDevice.State.INITIALIZED);
+
+ return true;
+ }
+
+ private void setUpdateState(GBDevice.State state) {
+ gbDevice.setState(state);
+ gbDevice.sendDeviceUpdateIntent(getContext());
+ }
+
+ private byte[] parseIncoming() throws IOException {
+ ByteArrayOutputStream msgStream = new ByteArrayOutputStream();
+
+ boolean finished = false;
+ ReaderState state = ReaderState.ID;
+ byte[] incoming = new byte[1];
+
+ while (!finished) {
+ mInStream.read(incoming);
+ msgStream.write(incoming);
+
+ switch (state) {
+ case ID:
+ state = ReaderState.HEADER_LEN;
+ incoming = new byte[1];
+ break;
+ case HEADER_LEN:
+ int headerSize = 0xff & incoming[0];
+ if (headerSize < 0)
+ throw new IOException();
+ state = ReaderState.HEADER;
+ incoming = new byte[headerSize];
+ break;
+ case HEADER:
+ int payloadSize = getLastInt(msgStream);
+ if (payloadSize < 0 || payloadSize > 8000) //this will possibly be changed in the future
+ throw new IOException();
+ state = ReaderState.PAYLOAD;
+ incoming = new byte[payloadSize];
+ break;
+ case PAYLOAD: //read is blocking, if we are here we have all the data
+ finished = true;
+ break;
+ }
+ }
+ byte[] msgArray = msgStream.toByteArray();
+ LOG.debug("received: " + GB.hexdump(msgArray, 0, msgArray.length));
+ return msgArray;
+ }
+
+
+ /**
+ * Enumeration containing the possible internal status of the reader.
+ */
+ private enum ReaderState {
+ ID, HEADER_LEN, HEADER, PAYLOAD;
+ }
+
+ private int getLastInt(ByteArrayOutputStream stream) {
+ byte[] array = stream.toByteArray();
+ ByteBuffer buffer = ByteBuffer.wrap(array, array.length - 4, 4);
+ buffer.order(LiveviewConstants.BYTE_ORDER);
+ return buffer.getInt();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewProtocol.java
new file mode 100644
index 000000000..27d38d7cd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewProtocol.java
@@ -0,0 +1,132 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
+
+import java.nio.ByteBuffer;
+import java.util.Calendar;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
+import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
+
+public class LiveviewProtocol extends GBDeviceProtocol {
+
+ @Override
+ public byte[] encodeFindDevice(boolean start) {
+ return encodeVibrateRequest((short) 100, (short) 200);
+ }
+
+ protected LiveviewProtocol(GBDevice device) {
+ super(device);
+ }
+
+ @Override
+ public GBDeviceEvent[] decodeResponse(byte[] responseData) {
+ int length = responseData.length;
+ if (length < 4) {
+ //empty message
+ return null;
+ } else {
+ ByteBuffer buffer = ByteBuffer.wrap(responseData, 0, length);
+ byte msgId = buffer.get();
+ buffer.get();
+ int payloadLen = buffer.getInt();
+ GBDeviceEventSendBytes reply = new GBDeviceEventSendBytes();
+ if (payloadLen + 6 == length) {
+ switch (msgId) {
+ case LiveviewConstants.MSG_DEVICESTATUS:
+ reply.encodedBytes = constructMessage(LiveviewConstants.MSG_DEVICESTATUS_ACK, new byte[]{LiveviewConstants.RESULT_OK});
+ break;
+ case LiveviewConstants.MSG_DISPLAYPANEL_ACK:
+ reply.encodedBytes = encodeVibrateRequest((short) 100, (short) 200); //hack to make the notifications vibrate!
+ break;
+ default:
+ }
+ GBDeviceEventSendBytes ack = new GBDeviceEventSendBytes();
+ ack.encodedBytes = constructMessage(LiveviewConstants.MSG_ACK, new byte[]{msgId});
+
+ return new GBDeviceEvent[]{ack, reply};
+ }
+ }
+
+
+ return super.decodeResponse(responseData);
+ }
+
+ @Override
+ public byte[] encodeSetTime() {
+ int time = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
+ time += Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 1000;
+ time += Calendar.getInstance().get(Calendar.DST_OFFSET) / 1000;
+ ByteBuffer buffer = ByteBuffer.allocate(5);
+ buffer.order(LiveviewConstants.BYTE_ORDER);
+ buffer.putInt(time);
+ buffer.put(LiveviewConstants.CLOCK_24H);
+ return constructMessage(LiveviewConstants.MSG_GETTIME_RESP, buffer.array());
+ }
+
+ @Override
+ public byte[] encodeNotification(NotificationSpec notificationSpec) {
+ String headerText;
+ // for SMS and EMAIL that came in though SMS or K9 receiver
+ if (notificationSpec.sender != null) {
+ headerText = notificationSpec.sender;
+ } else {
+ headerText = notificationSpec.title;
+ }
+
+ String footerText = (null != notificationSpec.sourceName) ? notificationSpec.sourceName : "";
+ String bodyText = (null != notificationSpec.body) ? notificationSpec.body : "";
+
+ byte[] headerTextArray = headerText.getBytes(LiveviewConstants.ENCODING);
+ byte[] footerTextArray = footerText.getBytes(LiveviewConstants.ENCODING);
+ byte[] bodyTextArray = bodyText.getBytes(LiveviewConstants.ENCODING);
+ int size = 15 + headerTextArray.length + bodyTextArray.length + footerTextArray.length;
+ ByteBuffer buffer = ByteBuffer.allocate(size);
+ buffer.put((byte) 1);
+ buffer.putShort((short) 0);
+ buffer.putShort((short) 0);
+ buffer.putShort((short) 0);
+ buffer.put((byte) 80); //should alert but it doesn't make the liveview vibrate
+
+ buffer.put((byte) 0); //0 is for plaintext vs bitmapimage (1) strings
+ buffer.putShort((short) headerTextArray.length);
+ buffer.put(headerTextArray);
+ buffer.putShort((short) bodyTextArray.length);
+ buffer.put(bodyTextArray);
+ buffer.putShort((short) footerTextArray.length);
+ buffer.put(footerTextArray);
+ return constructMessage(LiveviewConstants.MSG_DISPLAYPANEL, buffer.array());
+ }
+
+
+ //specific messages
+
+ public static byte[] constructMessage(byte messageType, byte[] payload) {
+ ByteBuffer msgBuffer = ByteBuffer.allocate(payload.length + 6);
+ msgBuffer.order(LiveviewConstants.BYTE_ORDER);
+ msgBuffer.put(messageType);
+ msgBuffer.put((byte) 4);
+ msgBuffer.putInt(payload.length);
+ msgBuffer.put(payload);
+ return msgBuffer.array();
+ }
+
+ public byte[] encodeVibrateRequest(short delay, short time) {
+ ByteBuffer buffer = ByteBuffer.allocate(4);
+ buffer.order(LiveviewConstants.BYTE_ORDER);
+ buffer.putShort(delay);
+ buffer.putShort(time);
+ return constructMessage(LiveviewConstants.MSG_SETVIBRATE, buffer.array());
+ }
+
+ public byte[] encodeCapabilitiesRequest() {
+ byte[] version = LiveviewConstants.CLIENT_SOFTWARE_VERSION.getBytes(LiveviewConstants.ENCODING);
+ ByteBuffer buffer = ByteBuffer.allocate(version.length + 1);
+ buffer.order(LiveviewConstants.BYTE_ORDER);
+ buffer.put((byte) version.length);
+ buffer.put(version);
+ return constructMessage(LiveviewConstants.MSG_GETCAPS, buffer.array());
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewSupport.java
new file mode 100644
index 000000000..08842be7b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewSupport.java
@@ -0,0 +1,106 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
+
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
+import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
+
+public class LiveviewSupport extends AbstractSerialDeviceSupport {
+
+ @Override
+ public boolean connect() {
+ getDeviceIOThread().start();
+ return true;
+ }
+
+ @Override
+ protected GBDeviceProtocol createDeviceProtocol() {
+ return new LiveviewProtocol(getDevice());
+ }
+
+ @Override
+ protected GBDeviceIoThread createDeviceIOThread() {
+ return new LiveviewIoThread(getDevice(), getContext(), getDeviceProtocol(), LiveviewSupport.this, getBluetoothAdapter());
+ }
+
+ @Override
+ public boolean useAutoConnect() {
+ return false;
+ }
+
+ @Override
+ public void onInstallApp(Uri uri) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onAppConfiguration(UUID uuid, String config) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onHeartRateTest() {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onSetConstantVibration(int intensity) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public synchronized LiveviewIoThread getDeviceIOThread() {
+ return (LiveviewIoThread) super.getDeviceIOThread();
+ }
+
+ @Override
+ public void onNotification(NotificationSpec notificationSpec) {
+ super.onNotification(notificationSpec);
+ }
+
+ @Override
+ public void onSetCallState(CallSpec callSpec) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onSetMusicState(MusicStateSpec musicStateSpec) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onSetMusicInfo(MusicSpec musicSpec) {
+ //nothing to do ATM
+ }
+
+
+ @Override
+ public void onSetAlarms(ArrayList extends Alarm> alarms) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onDeleteCalendarEvent(byte type, long id) {
+ //nothing to do ATM
+ }
+
+ @Override
+ public void onTestNewFunction() {
+ //nothing to do ATM
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java
index cbe8148c6..cdb1e1b69 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java
@@ -13,7 +13,7 @@ public abstract class AbstractMi1SFirmwareInfo extends AbstractMiFirmwareInfo {
@Override
public boolean isGenerallyCompatibleWith(GBDevice device) {
- return MiBandConst.MI_1S.equals(device.getHardwareVersion());
+ return MiBandConst.MI_1S.equals(device.getModel());
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java
index c1c36258b..3ba149df6 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java
@@ -83,7 +83,7 @@ public class DeviceInfo extends AbstractInfo {
}
public boolean supportsHeartrate() {
- return isMiliPro() || isMili1S() || (test1AHRMode && isMili1A());
+ return isMili1S() || (test1AHRMode && isMili1A());
}
@Override
@@ -116,10 +116,6 @@ public class DeviceInfo extends AbstractInfo {
return hwVersion == 6;
}
- public boolean isMiliPro() {
- return hwVersion == 8 || (feature == 8 && appearance == 0);
- }
-
public String getHwVersion() {
if (isMili1()) {
return MiBandConst.MI_1;
@@ -133,9 +129,6 @@ public class DeviceInfo extends AbstractInfo {
if (isAmazFit()) {
return MiBandConst.MI_AMAZFIT;
}
- if (isMiliPro()) {
- return MiBandConst.MI_PRO;
- }
return "?";
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java
index ce975245c..e8dbf6414 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java
@@ -31,7 +31,7 @@ public class Mi1AFirmwareInfo extends AbstractMi1FirmwareInfo {
@Override
public boolean isGenerallyCompatibleWith(GBDevice device) {
- String hwVersion = device.getHardwareVersion();
+ String hwVersion = device.getModel();
return MiBandConst.MI_1A.equals(hwVersion);
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java
index aa5067813..37fde0629 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java
@@ -31,7 +31,7 @@ public class Mi1FirmwareInfo extends AbstractMi1FirmwareInfo {
@Override
public boolean isGenerallyCompatibleWith(GBDevice device) {
- String hwVersion = device.getHardwareVersion();
+ String hwVersion = device.getModel();
return MiBandConst.MI_1.equals(hwVersion);
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java
index 926afca19..8ef2e65da 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java
@@ -30,7 +30,7 @@ public class Mi1SFirmwareInfo extends CompositeMiFirmwareInfo {
@Override
public boolean isGenerallyCompatibleWith(GBDevice device) {
- return MiBandConst.MI_1S.equals(device.getHardwareVersion());
+ return MiBandConst.MI_1S.equals(device.getModel());
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java
new file mode 100644
index 000000000..e6466471b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java
@@ -0,0 +1,1277 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.support.v4.content.LocalBroadcastManager;
+import android.widget.Toast;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
+import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents;
+import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
+import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2NotificationStrategy;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PROFILE;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COLOUR;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue;
+import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue;
+
+public class MiBand2Support extends AbstractBTLEDeviceSupport {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MiBand2Support.class);
+ private final DeviceInfoProfile deviceInfoProfile;
+ private final HeartRateProfile heartRateProfile;
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String s = intent.getAction();
+ if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) {
+ handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
+ }
+ }
+ };
+
+ private boolean needsAuth;
+ private volatile boolean telephoneRinging;
+ private volatile boolean isLocatingDevice;
+
+ private DeviceInfo mDeviceInfo;
+
+ private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
+ private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
+ private RealtimeSamplesSupport realtimeSamplesSupport;
+
+ public MiBand2Support() {
+ super(LOG);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
+ addSupportedService(GattService.UUID_SERVICE_HEART_RATE);
+ addSupportedService(GattService.UUID_SERVICE_IMMEDIATE_ALERT);
+ addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
+ addSupportedService(GattService.UUID_SERVICE_ALERT_NOTIFICATION);
+
+ addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE);
+ addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
+
+ deviceInfoProfile = new DeviceInfoProfile<>(this);
+ addSupportedProfile(deviceInfoProfile);
+ heartRateProfile = new HeartRateProfile<>(this);
+ addSupportedProfile(heartRateProfile);
+
+ LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO);
+ intentFilter.addAction(DeviceService.ACTION_MIBAND2_AUTH);
+ broadcastManager.registerReceiver(mReceiver, intentFilter);
+ }
+
+ @Override
+ public void dispose() {
+ LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
+ broadcastManager.unregisterReceiver(mReceiver);
+ super.dispose();
+ }
+
+ @Override
+ protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
+ try {
+ boolean authenticate = needsAuth;
+ needsAuth = false;
+ new InitOperation(authenticate, this, builder).perform();
+ } catch (IOException e) {
+ GB.toast(getContext(), "Initializing Mi Band 2 failed", Toast.LENGTH_SHORT, GB.ERROR, e);
+ }
+
+// builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZING, getContext()));
+// enableNotifications(builder, true)
+// .setLowLatency(builder)
+// .readDate(builder) // without reading the data, we get sporadic connection problems, especially directly after turning on BT
+// this is apparently not needed anymore, and actually causes problems when bonding is not used/does not work
+// so we simply not use the UUID_PAIR characteristic.
+// .pair(builder)
+ //.requestDeviceInfo(builder)
+ //.requestBatteryInfo(builder);
+// .sendUserInfo(builder)
+// .checkAuthenticationNeeded(builder, getDevice())
+// .setWearLocation(builder)
+// .setHeartrateSleepSupport(builder)
+// .setFitnessGoal(builder)
+// .enableFurtherNotifications(builder, true)
+// .setCurrentTime(builder)
+// .requestBatteryInfo(builder)
+// .setHighLatency(builder)
+// .setInitialized(builder);
+ return builder;
+ }
+
+ public byte[] getTimeBytes(Calendar calendar, TimeUnit precision) {
+ byte[] bytes;
+ if (precision == TimeUnit.MINUTES) {
+ bytes = BLETypeConversions.shortCalendarToRawBytes(calendar, true);
+ } else if (precision == TimeUnit.SECONDS) {
+ bytes = BLETypeConversions.calendarToRawBytes(calendar, true);
+ } else {
+ throw new IllegalArgumentException("Unsupported precision, only MINUTES and SECONDS are supported till now");
+ }
+ byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone
+// byte[] tail = new byte[] { 0x2 }; // reason
+ byte[] all = BLETypeConversions.join(bytes, tail);
+ return all;
+ }
+
+ public Calendar fromTimeBytes(byte[] bytes) {
+ GregorianCalendar timestamp = BLETypeConversions.rawBytesToCalendar(bytes, true);
+ return timestamp;
+ }
+
+ public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) {
+ GregorianCalendar now = BLETypeConversions.createCalendar();
+ byte[] bytes = getTimeBytes(now, TimeUnit.SECONDS);
+ builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytes);
+
+// byte[] localtime = BLETypeConversions.calendarToLocalTimeBytes(now);
+// builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_LOCAL_TIME_INFORMATION), localtime);
+// builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), new byte[] {0x2, 0x00});
+// builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x03,0x00,(byte)0x8e,(byte)0xce,0x5a,0x09,(byte)0xb3,(byte)0xd8,0x55,0x57,0x10,0x2a,(byte)0xed,0x7d,0x6b,0x78,(byte)0xc5,(byte)0xd2});
+ return this;
+ }
+
+ private MiBand2Support readDate(TransactionBuilder builder) {
+ // NAVL
+// builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DATE_TIME));
+ // TODO: handle result
+ builder.read(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME));
+ return this;
+ }
+
+ // NAVL
+ public MiBand2Support setLowLatency(TransactionBuilder builder) {
+// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getLowLatency());
+ return this;
+ }
+ // NAVL
+ public MiBand2Support setHighLatency(TransactionBuilder builder) {
+// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getHighLatency());
+ return this;
+ }
+
+ private MiBand2Support checkAuthenticationNeeded(TransactionBuilder builder, GBDevice device) {
+ builder.add(new CheckAuthenticationNeededAction(device));
+ return this;
+ }
+
+ /**
+ * Last action of initialization sequence. Sets the device to initialized.
+ * It is only invoked if all other actions were successfully run, so the device
+ * must be initialized, then.
+ *
+ * @param builder
+ */
+ public void setInitialized(TransactionBuilder builder) {
+ builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZED, getContext()));
+ }
+
+ // MB2: AVL
+ // TODO: tear down the notifications on quit
+ public MiBand2Support enableNotifications(TransactionBuilder builder, boolean enable) {
+ builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable);
+ builder.notify(getCharacteristic(GattService.UUID_SERVICE_CURRENT_TIME), enable);
+ // Notify CHARACTERISTIC9 to receive random auth code
+ builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), enable);
+ return this;
+ }
+
+ public MiBand2Support enableFurtherNotifications(TransactionBuilder builder, boolean enable) {
+// builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
+// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable)
+// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable)
+// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
+ builder.notify(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), enable);
+ BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT);
+ if (heartrateCharacteristic != null) {
+ builder.notify(heartrateCharacteristic, enable);
+ }
+
+ return this;
+ }
+
+ @Override
+ public boolean useAutoConnect() {
+ return true;
+ }
+
+ @Override
+ public void pair() {
+ needsAuth = true;
+ for (int i = 0; i < 5; i++) {
+ if (connect()) {
+ return;
+ }
+ }
+ }
+
+ public DeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ private MiBand2Support sendDefaultNotification(TransactionBuilder builder, short repeat, BtLEAction extraAction) {
+ LOG.info("Sending notification to MiBand: (" + repeat + " times)");
+ NotificationStrategy strategy = getNotificationStrategy();
+ for (short i = 0; i < repeat; i++) {
+ strategy.sendDefaultNotification(builder, extraAction);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a custom notification to the given transaction builder
+ *
+ * @param vibrationProfile specifies how and how often the Band shall vibrate.
+ * @param flashTimes
+ * @param flashColour
+ * @param originalColour
+ * @param flashDuration
+ * @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example.
+ * @param builder
+ */
+ private MiBand2Support sendCustomNotification(VibrationProfile vibrationProfile, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) {
+ getNotificationStrategy().sendCustomNotification(vibrationProfile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder);
+ LOG.info("Sending notification to MiBand");
+ return this;
+ }
+
+ private NotificationStrategy getNotificationStrategy() {
+ return new Mi2NotificationStrategy(this);
+ }
+
+ static final byte[] reboot = new byte[]{MiBandService.COMMAND_REBOOT};
+
+ static final byte[] startHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 1};
+ static final byte[] stopHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 0};
+ static final byte[] startHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 1};
+ static final byte[] stopHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 0};
+ static final byte[] startHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 1};
+ static final byte[] stopHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 0};
+
+ static final byte[] startRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 1};
+ static final byte[] stopRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 0};
+
+ /**
+ * Part of device initialization process. Do not call manually.
+ *
+ * @param builder
+ * @return
+ */
+ private MiBand2Support sendUserInfo(TransactionBuilder builder) {
+ LOG.debug("Writing User Info!");
+ // Use a custom action instead of just builder.write() because mDeviceInfo
+ // is set by handleDeviceInfo *after* this action is created.
+ builder.add(new BtLEAction(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_USER_INFO)) {
+ @Override
+ public boolean expectsResult() {
+ return true;
+ }
+
+ @Override
+ public boolean run(BluetoothGatt gatt) {
+ // at this point, mDeviceInfo should be set
+ return new WriteAction(getCharacteristic(),
+ MiBandCoordinator.getAnyUserInfo(getDevice().getAddress()).getData(mDeviceInfo)
+ ).run(gatt);
+ }
+ });
+ return this;
+ }
+
+ public MiBand2Support requestDeviceInfo(TransactionBuilder builder) {
+ LOG.debug("Requesting Device Info!");
+ deviceInfoProfile.requestDeviceInfo(builder);
+ return this;
+ }
+
+ /* private MiBandSupport requestHRInfo(TransactionBuilder builder) {
+ LOG.debug("Requesting HR Info!");
+ BluetoothGattCharacteristic HRInfo = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT);
+ builder.read(HRInfo);
+ BluetoothGattCharacteristic HR_Point = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT);
+ builder.read(HR_Point);
+ return this;
+ }
+ *//**
+ * Part of HR test. Do not call manually.
+ *
+ * @param transaction
+ * @return
+ *//*
+ private MiBandSupport heartrate(TransactionBuilder transaction) {
+ LOG.info("Attempting to read HR ...");
+ BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT);
+ if (characteristic != null) {
+ transaction.write(characteristic, new byte[]{MiBandService.COMMAND_SET__HR_CONTINUOUS});
+ } else {
+ LOG.info("Unable to read HR from MI device -- characteristic not available");
+ }
+ return this;
+ }*/
+
+ /**
+ * Part of device initialization process. Do not call manually.
+ *
+ * @param transaction
+ * @return
+ */
+
+ private MiBand2Support setFitnessGoal(TransactionBuilder transaction) {
+ LOG.info("Attempting to set Fitness Goal...");
+ BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8);
+ if (characteristic != null) {
+ int fitnessGoal = MiBandCoordinator.getFitnessGoal(getDevice().getAddress());
+ byte[] bytes = ArrayUtils.addAll(
+ MiBand2Service.COMMAND_SET_FITNESS_GOAL_START,
+ BLETypeConversions.fromUint16(fitnessGoal));
+ bytes = ArrayUtils.addAll(bytes,
+ MiBand2Service.COMMAND_SET_FITNESS_GOAL_END);
+ transaction.write(characteristic, bytes);
+ } else {
+ LOG.info("Unable to set Fitness Goal");
+ }
+ return this;
+ }
+
+ /**
+ * Part of device initialization process. Do not call manually.
+ *
+ * @param builder
+ * @return
+ */
+ private MiBand2Support setWearLocation(TransactionBuilder builder) {
+ LOG.info("Attempting to set wear location...");
+ BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8);
+ if (characteristic != null) {
+ builder.notify(characteristic, true);
+ int location = MiBandCoordinator.getWearLocation(getDevice().getAddress());
+ switch (location) {
+ case 0: // left hand
+ builder.write(characteristic, MiBand2Service.WEAR_LOCATION_LEFT_WRIST);
+ break;
+ case 1: // right hand
+ builder.write(characteristic, MiBand2Service.WEAR_LOCATION_RIGHT_WRIST);
+ break;
+ }
+ builder.notify(characteristic, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed.
+ }
+ return this;
+ }
+
+ @Override
+ public void onEnableHeartRateSleepSupport(boolean enable) {
+ try {
+ TransactionBuilder builder = performInitialized("enable heart rate sleep support: " + enable);
+ setHeartrateSleepSupport(builder);
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ GB.toast(getContext(), "Error toggling heart rate sleep support: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
+ }
+ }
+
+ @Override
+ public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
+ // not supported
+ }
+
+ @Override
+ public void onDeleteCalendarEvent(byte type, long id) {
+ // not supported
+ }
+
+ /**
+ * Part of device initialization process. Do not call manually.
+ *
+ * @param builder
+ */
+ private MiBand2Support setHeartrateSleepSupport(TransactionBuilder builder) {
+ BluetoothGattCharacteristic characteristicHRControlPoint = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT);
+ final boolean enableHrSleepSupport = MiBandCoordinator.getHeartrateSleepSupport(getDevice().getAddress());
+ if (characteristicHRControlPoint != null) {
+ builder.notify(characteristicHRControlPoint, true);
+ if (enableHrSleepSupport) {
+ LOG.info("Enabling heartrate sleep support...");
+ builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_ENABLE_HR_SLEEP_MEASUREMENT);
+ } else {
+ LOG.info("Disabling heartrate sleep support...");
+ builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_DISABLE_HR_SLEEP_MEASUREMENT);
+ }
+ builder.notify(characteristicHRControlPoint, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed.
+ }
+ return this;
+ }
+
+ private void performDefaultNotification(String task, short repeat, BtLEAction extraAction) {
+ try {
+ TransactionBuilder builder = performInitialized(task);
+ sendDefaultNotification(builder, repeat, extraAction);
+ builder.queue(getQueue());
+ } catch (IOException ex) {
+ LOG.error("Unable to send notification to MI device", ex);
+ }
+ }
+
+ private void performPreferredNotification(String task, String notificationOrigin, int alertLevel, BtLEAction extraAction) {
+ try {
+ TransactionBuilder builder = performInitialized(task);
+ Prefs prefs = GBApplication.getPrefs();
+ int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs);
+ int vibratePause = getPreferredVibratePause(notificationOrigin, prefs);
+ short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs);
+ VibrationProfile profile = getPreferredVibrateProfile(notificationOrigin, prefs, vibrateTimes);
+ profile.setAlertLevel(alertLevel);
+
+ int flashTimes = getPreferredFlashCount(notificationOrigin, prefs);
+ int flashColour = getPreferredFlashColour(notificationOrigin, prefs);
+ int originalColour = getPreferredOriginalColour(notificationOrigin, prefs);
+ int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs);
+
+ sendCustomNotification(profile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder);
+// sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder);
+ builder.queue(getQueue());
+ } catch (IOException ex) {
+ LOG.error("Unable to send notification to MI device", ex);
+ }
+ }
+
+ private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) {
+ return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION);
+ }
+
+ private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) {
+ return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR);
+ }
+
+ private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) {
+ return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR);
+ }
+
+ private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) {
+ return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT);
+ }
+
+ private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) {
+ return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE);
+ }
+
+ private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) {
+ return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT));
+ }
+
+ private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) {
+ return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION);
+ }
+
+ private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) {
+ String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE);
+ return VibrationProfile.getProfile(profileId, repeat);
+ }
+
+ @Override
+ public void onSetAlarms(ArrayList extends Alarm> alarms) {
+ try {
+ BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3);
+ TransactionBuilder builder = performInitialized("Set alarm");
+ boolean anyAlarmEnabled = false;
+ for (Alarm alarm : alarms) {
+ anyAlarmEnabled |= alarm.isEnabled();
+ queueAlarm(alarm, builder, characteristic);
+ }
+ builder.queue(getQueue());
+ if (anyAlarmEnabled) {
+ GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO);
+ } else {
+ GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO);
+ }
+ } catch (IOException ex) {
+ GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_failed), Toast.LENGTH_LONG, GB.ERROR, ex);
+ }
+ }
+
+ @Override
+ public void onNotification(NotificationSpec notificationSpec) {
+ int alertLevel = MiBand2Service.ALERT_LEVEL_MESSAGE;
+ if (notificationSpec.type == NotificationType.UNKNOWN) {
+ alertLevel = MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY;
+ }
+ String origin = notificationSpec.type.getGenericType();
+ performPreferredNotification(origin + " received", origin, alertLevel, null);
+ }
+
+ @Override
+ public void onSetTime() {
+ try {
+ TransactionBuilder builder = performInitialized("Set date and time");
+ setCurrentTimeWithService(builder);
+ //TODO: once we have a common strategy for sending events (e.g. EventHandler), remove this call from here. Meanwhile it does no harm.
+ sendCalendarEvents(builder);
+ builder.queue(getQueue());
+ } catch (IOException ex) {
+ LOG.error("Unable to set time on MI device", ex);
+ }
+ }
+
+ @Override
+ public void onSetCallState(CallSpec callSpec) {
+ if (callSpec.command == CallSpec.CALL_INCOMING) {
+ telephoneRinging = true;
+ AbortTransactionAction abortAction = new AbortTransactionAction() {
+ @Override
+ protected boolean shouldAbort() {
+ return !isTelephoneRinging();
+ }
+
+ @Override
+ public boolean run(BluetoothGatt gatt) {
+ if (!super.run(gatt)) {
+ // send a signal to stop the vibration
+ BluetoothGattCharacteristic characteristic = MiBand2Support.this.getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL);
+ characteristic.setValue(new byte[] {MiBand2Service.ALERT_LEVEL_NONE});
+ gatt.writeCharacteristic(characteristic);
+ return false;
+ }
+ return true;
+ }
+ };
+ performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, MiBand2Service.ALERT_LEVEL_PHONE_CALL, abortAction);
+ } else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) {
+ telephoneRinging = false;
+ }
+ }
+
+ @Override
+ public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
+ }
+
+ private boolean isTelephoneRinging() {
+ // don't synchronize, this is not really important
+ return telephoneRinging;
+ }
+
+ @Override
+ public void onSetMusicState(MusicStateSpec stateSpec) {
+ // not supported
+ }
+
+ @Override
+ public void onSetMusicInfo(MusicSpec musicSpec) {
+ // not supported
+ }
+
+ @Override
+ public void onReboot() {
+ try {
+ TransactionBuilder builder = performInitialized("Reboot");
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), reboot);
+ builder.queue(getQueue());
+ } catch (IOException ex) {
+ LOG.error("Unable to reboot MI", ex);
+ }
+ }
+
+ @Override
+ public void onHeartRateTest() {
+ try {
+ TransactionBuilder builder = performInitialized("HeartRateTest");
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous);
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual);
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementManual);
+ builder.queue(getQueue());
+ } catch (IOException ex) {
+ LOG.error("Unable to read HearRate with MI2", ex);
+ }
+ }
+
+ @Override
+ public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
+ try {
+ TransactionBuilder builder = performInitialized("Enable realtime heart rateM measurement");
+ if (enable) {
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual);
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementContinuous);
+ } else {
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous);
+ }
+ builder.queue(getQueue());
+ enableRealtimeSamplesTimer(enable);
+ } catch (IOException ex) {
+ LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex);
+ }
+ }
+
+ @Override
+ public void onFindDevice(boolean start) {
+ isLocatingDevice = start;
+
+ if (start) {
+ AbortTransactionAction abortAction = new AbortTransactionAction() {
+ @Override
+ protected boolean shouldAbort() {
+ return !isLocatingDevice;
+ }
+ };
+ performDefaultNotification("locating device", (short) 255, abortAction);
+ }
+ }
+
+ @Override
+ public void onSetConstantVibration(int intensity) {
+
+ }
+
+ @Override
+ public void onFetchActivityData() {
+ try {
+ new FetchActivityOperation(this).perform();
+ } catch (IOException ex) {
+ LOG.error("Unable to fetch MI activity data", ex);
+ }
+ }
+
+ @Override
+ public void onEnableRealtimeSteps(boolean enable) {
+// try {
+// BluetoothGattCharacteristic controlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT);
+// if (enable) {
+// TransactionBuilder builder = performInitialized("Read realtime steps");
+// builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS)).queue(getQueue());
+// }
+// performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications")
+// .write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), enable ? getLowLatency() : getHighLatency())
+// .write(controlPoint, enable ? startRealTimeStepsNotifications : stopRealTimeStepsNotifications).queue(getQueue());
+// enableRealtimeSamplesTimer(enable);
+// } catch (IOException e) {
+// LOG.error("Unable to change realtime steps notification to: " + enable, e);
+// }
+ }
+
+ private byte[] getHighLatency() {
+ int minConnectionInterval = 460;
+ int maxConnectionInterval = 500;
+ int latency = 0;
+ int timeout = 500;
+ int advertisementInterval = 0;
+
+ return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval);
+ }
+
+ private byte[] getLatency(int minConnectionInterval, int maxConnectionInterval, int latency, int timeout, int advertisementInterval) {
+ byte result[] = new byte[12];
+ result[0] = (byte) (minConnectionInterval & 0xff);
+ result[1] = (byte) (0xff & minConnectionInterval >> 8);
+ result[2] = (byte) (maxConnectionInterval & 0xff);
+ result[3] = (byte) (0xff & maxConnectionInterval >> 8);
+ result[4] = (byte) (latency & 0xff);
+ result[5] = (byte) (0xff & latency >> 8);
+ result[6] = (byte) (timeout & 0xff);
+ result[7] = (byte) (0xff & timeout >> 8);
+ result[8] = 0;
+ result[9] = 0;
+ result[10] = (byte) (advertisementInterval & 0xff);
+ result[11] = (byte) (0xff & advertisementInterval >> 8);
+
+ return result;
+ }
+
+ private byte[] getLowLatency() {
+ int minConnectionInterval = 39;
+ int maxConnectionInterval = 49;
+ int latency = 0;
+ int timeout = 500;
+ int advertisementInterval = 0;
+
+ return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval);
+ }
+
+ @Override
+ public void onInstallApp(Uri uri) {
+// TODO: onInstallApp (firmware update)
+// try {
+// new UpdateFirmwareOperation(uri, this).perform();
+// } catch (IOException ex) {
+// GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
+// }
+ }
+
+ @Override
+ public void onAppInfoReq() {
+ // not supported
+ }
+
+ @Override
+ public void onAppStart(UUID uuid, boolean start) {
+ // not supported
+ }
+
+ @Override
+ public void onAppDelete(UUID uuid) {
+ // not supported
+ }
+
+ @Override
+ public void onAppConfiguration(UUID uuid, String config) {
+ // not supported
+ }
+
+ @Override
+ public void onAppReorder(UUID[] uuids) {
+ // not supported
+ }
+
+ @Override
+ public void onScreenshotReq() {
+ // not supported
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ super.onCharacteristicChanged(gatt, characteristic);
+
+ UUID characteristicUUID = characteristic.getUuid();
+ if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
+ handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS);
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) {
+ handleNotificationNotif(characteristic.getValue());
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) {
+ handleRealtimeSteps(characteristic.getValue());
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) {
+ handleHeartrate(characteristic.getValue());
+ return true;
+// } else if (MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0.equals(characteristicUUID)) {
+// handleUnknownCharacteristic(characteristic.getValue());
+// return true;
+ } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) {
+ LOG.info("AUTHENTICATION?? " + characteristicUUID);
+ logMessageContent(characteristic.getValue());
+ return true;
+ } else {
+ LOG.info("Unhandled characteristic changed: " + characteristicUUID);
+ logMessageContent(characteristic.getValue());
+ }
+ return false;
+ }
+
+ private void handleUnknownCharacteristic(byte[] value) {
+
+ }
+
+ @Override
+ public boolean onCharacteristicRead(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ super.onCharacteristicRead(gatt, characteristic, status);
+
+ UUID characteristicUUID = characteristic.getUuid();
+ if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) {
+ handleDeviceName(characteristic.getValue(), status);
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
+ handleBatteryInfo(characteristic.getValue(), status);
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) {
+ logHeartrate(characteristic.getValue(), status);
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_DATE_TIME.equals(characteristicUUID)) {
+ logDate(characteristic.getValue(), status);
+ return true;
+ } else {
+ LOG.info("Unhandled characteristic read: " + characteristicUUID);
+ logMessageContent(characteristic.getValue());
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onCharacteristicWrite(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ UUID characteristicUUID = characteristic.getUuid();
+ if (MiBandService.UUID_CHARACTERISTIC_PAIR.equals(characteristicUUID)) {
+ handlePairResult(characteristic.getValue(), status);
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_USER_INFO.equals(characteristicUUID)) {
+ handleUserInfoResult(characteristic.getValue(), status);
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT.equals(characteristicUUID)) {
+ handleControlPointResult(characteristic.getValue(), status);
+ return true;
+ } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) {
+ LOG.info("KEY AES SEND");
+ logMessageContent(characteristic.getValue());
+ return true;
+ }
+ return false;
+ }
+
+ public void logDate(byte[] value, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value);
+ LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime()));
+ } else {
+ logMessageContent(value);
+ }
+ }
+
+ public void logHeartrate(byte[] value, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS && value != null) {
+ LOG.info("Got heartrate:");
+ if (value.length == 2 && value[0] == 0) {
+ int hrValue = (value[1] & 0xff);
+ GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO);
+ }
+ return;
+ }
+ logMessageContent(value);
+ }
+
+ private void handleHeartrate(byte[] value) {
+ if (value.length == 2 && value[0] == 0) {
+ int hrValue = (value[1] & 0xff);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("heart rate: " + hrValue);
+ }
+ RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport();
+ realtimeSamplesSupport.setHeartrateBpm(hrValue);
+ if (!realtimeSamplesSupport.isRunning()) {
+ // single shot measurement, manually invoke storage and result publishing
+ realtimeSamplesSupport.triggerCurrentSample();
+ }
+ }
+ }
+
+ private void handleRealtimeSteps(byte[] value) {
+ int steps = 0xff & value[0] | (0xff & value[1]) << 8;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("realtime steps: " + steps);
+ }
+ getRealtimeSamplesSupport().setSteps(steps);
+ }
+
+ private void enableRealtimeSamplesTimer(boolean enable) {
+ if (enable) {
+ getRealtimeSamplesSupport().start();
+ } else {
+ if (realtimeSamplesSupport != null) {
+ realtimeSamplesSupport.stop();
+ }
+ }
+ }
+
+ public MiBandActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) {
+ MiBandActivitySample sample = new MiBandActivitySample();
+ sample.setDevice(device);
+ sample.setUser(user);
+ sample.setTimestamp(timestampInSeconds);
+ sample.setProvider(provider);
+
+ return sample;
+ }
+
+ private RealtimeSamplesSupport getRealtimeSamplesSupport() {
+ if (realtimeSamplesSupport == null) {
+ realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) {
+ @Override
+ public void doCurrentSample() {
+
+ try (DBHandler handler = GBApplication.acquireDB()) {
+ DaoSession session = handler.getDaoSession();
+
+ Device device = DBHelper.getDevice(getDevice(), session);
+ User user = DBHelper.getUser(session);
+ int ts = (int) (System.currentTimeMillis() / 1000);
+ MiBand2SampleProvider provider = new MiBand2SampleProvider(gbDevice, session);
+ MiBandActivitySample sample = createActivitySample(device, user, ts, provider);
+ sample.setHeartRate(getHeartrateBpm());
+ sample.setSteps(getSteps());
+ sample.setRawIntensity(ActivitySample.NOT_MEASURED);
+ sample.setRawKind(MiBand2SampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that?
+
+ // TODO: remove this once fully ported to REALTIME_SAMPLES
+ if (sample.getSteps() != ActivitySample.NOT_MEASURED) {
+ Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS)
+ .putExtra(DeviceService.EXTRA_REALTIME_STEPS, sample.getSteps())
+ .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ }
+ if (sample.getHeartRate() != ActivitySample.NOT_MEASURED) {
+ Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT)
+ .putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, sample.getHeartRate())
+ .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ }
+
+// Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
+// .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
+// LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+
+ LOG.debug("Storing realtime sample: " + sample);
+ provider.addGBActivitySample(sample);
+ } catch (Exception e) {
+ LOG.warn("Unable to acquire db for saving realtime samples", e);
+ }
+ }
+ };
+ }
+ return realtimeSamplesSupport;
+ }
+
+ /**
+ * React to unsolicited messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION
+ * characteristic,
+ * These messages appear to be always 1 byte long, with values that are listed in MiBandService.
+ * It is not excluded that there are further values which are still unknown.
+ *
+ * Upon receiving known values that request further action by GB, the appropriate method is called.
+ *
+ * @param value
+ */
+ private void handleNotificationNotif(byte[] value) {
+ if (value.length != 1) {
+ LOG.error("Notifications should be 1 byte long.");
+ LOG.info("RECEIVED DATA WITH LENGTH: " + value.length);
+ for (byte b : value) {
+ LOG.warn("DATA: " + String.format("0x%2x", b));
+ }
+ return;
+ }
+ switch (value[0]) {
+ case MiBandService.NOTIFY_AUTHENTICATION_FAILED:
+ // we get first FAILED, then NOTIFY_STATUS_MOTOR_AUTH (0x13)
+ // which means, we need to authenticate by tapping
+ getDevice().setState(State.AUTHENTICATION_REQUIRED);
+ getDevice().sendDeviceUpdateIntent(getContext());
+ GB.toast(getContext(), "Band needs pairing", Toast.LENGTH_LONG, GB.ERROR);
+ break;
+ case MiBandService.NOTIFY_AUTHENTICATION_SUCCESS: // fall through -- not sure which one we get
+ case MiBandService.NOTIFY_RESET_AUTHENTICATION_SUCCESS: // for Mi 1A
+ case MiBandService.NOTIFY_STATUS_MOTOR_AUTH_SUCCESS:
+ LOG.info("Band successfully authenticated");
+ // maybe we can perform the rest of the initialization from here
+ doInitialize();
+ break;
+
+ case MiBandService.NOTIFY_STATUS_MOTOR_AUTH:
+ LOG.info("Band needs authentication (MOTOR_AUTH)");
+ getDevice().setState(State.AUTHENTICATING);
+ getDevice().sendDeviceUpdateIntent(getContext());
+ break;
+
+ case MiBandService.NOTIFY_SET_LATENCY_SUCCESS:
+ LOG.info("Setting latency succeeded.");
+ break;
+ default:
+ for (byte b : value) {
+ LOG.warn("DATA: " + String.format("0x%2x", b));
+ }
+ }
+ }
+
+ private void doInitialize() {
+ try {
+ TransactionBuilder builder = performInitialized("just initializing after authentication");
+ builder.queue(getQueue());
+ } catch (IOException ex) {
+ LOG.error("Unable to initialize device after authentication", ex);
+ }
+ }
+
+ private void handleDeviceName(byte[] value, int status) {
+// if (status == BluetoothGatt.GATT_SUCCESS) {
+// versionCmd.hwVersion = new String(value);
+// handleGBDeviceEvent(versionCmd);
+// }
+ }
+
+ /**
+ * Convert an alarm from the GB internal structure to a Mi Band message and put on the specified
+ * builder queue as a write message for the passed characteristic
+ *
+ * @param alarm
+ * @param builder
+ * @param characteristic
+ */
+ private void queueAlarm(Alarm alarm, TransactionBuilder builder, BluetoothGattCharacteristic characteristic) {
+ Calendar calendar = alarm.getAlarmCal();
+
+ int maxAlarms = 5; // arbitrary at the moment...
+ if (alarm.getIndex() >= maxAlarms) {
+ if (alarm.isEnabled()) {
+ GB.toast(getContext(), "Only 5 alarms are currently supported.", Toast.LENGTH_LONG, GB.WARN);
+ }
+ return;
+ }
+
+ int base = 0;
+ if (alarm.isEnabled()) {
+ base = 128;
+ }
+ int daysMask = alarm.getRepetitionMask();
+ if (!alarm.isRepetitive()) {
+ daysMask = 128;
+ }
+ byte[] alarmMessage = new byte[] {
+ (byte) 0x2, // TODO what is this?
+ (byte) (base + alarm.getIndex()), // 128 is the base, alarm slot is added
+ (byte) calendar.get(Calendar.HOUR_OF_DAY),
+ (byte) calendar.get(Calendar.MINUTE),
+ (byte) daysMask,
+ };
+ builder.write(characteristic, alarmMessage);
+ // TODO: react on 0x10, 0x02, 0x01 on notification (success)
+ }
+
+ private void handleControlPointResult(byte[] value, int status) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ LOG.warn("Could not write to the control point.");
+ }
+ LOG.info("handleControlPoint write status:" + status + "; length: " + (value != null ? value.length : "(null)"));
+
+ if (value != null) {
+ for (byte b : value) {
+ LOG.info("handleControlPoint WROTE DATA:" + String.format("0x%8x", b));
+ }
+ } else {
+ LOG.warn("handleControlPoint WROTE null");
+ }
+ }
+
+ private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) {
+// if (getDeviceInfo().supportsHeartrate()) {
+// getDevice().addDeviceInfo(new GenericItem(
+// getContext().getString(R.string.DEVINFO_HR_VER),
+// info.getSoftwareRevision()));
+// }
+ LOG.warn("Device info: " + info);
+ versionCmd.hwVersion = info.getHardwareRevision();
+// versionCmd.fwVersion = info.getFirmwareRevision(); // always null
+ versionCmd.fwVersion = info.getSoftwareRevision();
+ handleGBDeviceEvent(versionCmd);
+ }
+
+ private void handleBatteryInfo(byte[] value, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ BatteryInfo info = new BatteryInfo(value);
+ batteryCmd.level = ((short) info.getLevelInPercent());
+ batteryCmd.state = info.getState();
+ batteryCmd.lastChargeTime = info.getLastChargeTime();
+ batteryCmd.numCharges = info.getNumCharges();
+ handleGBDeviceEvent(batteryCmd);
+ }
+ }
+
+ private void handleUserInfoResult(byte[] value, int status) {
+ // successfully transferred user info means we're initialized
+// commented out, because we have SetDeviceStateAction which sets initialized
+// state on every successful initialization.
+// if (status == BluetoothGatt.GATT_SUCCESS) {
+// setConnectionState(State.INITIALIZED);
+// }
+ }
+
+ private void setConnectionState(State newState) {
+ getDevice().setState(newState);
+ getDevice().sendDeviceUpdateIntent(getContext());
+ }
+
+ private void handlePairResult(byte[] pairResult, int status) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ LOG.info("Pairing MI device failed: " + status);
+ return;
+ }
+
+ String value = null;
+ if (pairResult != null) {
+ if (pairResult.length == 1) {
+ try {
+ if (pairResult[0] == 2) {
+ LOG.info("Successfully paired MI device");
+ return;
+ }
+ } catch (Exception ex) {
+ LOG.warn("Error identifying pairing result", ex);
+ return;
+ }
+ }
+ value = Arrays.toString(pairResult);
+ }
+ LOG.info("MI Band pairing result: " + value);
+ }
+
+ /**
+ * Fetch the events from the android device calendars and set the alarms on the miband.
+ * @param builder
+ */
+ private MiBand2Support sendCalendarEvents(TransactionBuilder builder) {
+ BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3);
+
+ Prefs prefs = GBApplication.getPrefs();
+ int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
+
+ if (availableSlots > 0) {
+ CalendarEvents upcomingEvents = new CalendarEvents();
+ List mEvents = upcomingEvents.getCalendarEventList(getContext());
+
+ int iteration = 0;
+ for (CalendarEvents.CalendarEvent mEvt : mEvents) {
+ if (iteration >= availableSlots || iteration > 2) {
+ break;
+ }
+ int slotToUse = 2 - iteration;
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(mEvt.getBegin());
+ Alarm alarm = GBAlarm.createSingleShot(slotToUse, false, calendar);
+ queueAlarm(alarm, builder, characteristic);
+ iteration++;
+ }
+ builder.queue(getQueue());
+ }
+ return this;
+ }
+
+ @Override
+ public void onSendConfiguration(String config) {
+ TransactionBuilder builder = null;
+ try {
+ builder = performInitialized("Sending configuration for option: " + config);
+ switch (config) {
+ case MiBandConst.PREF_MI2_DATEFORMAT:
+ setDateDisplay(builder);
+ break;
+ case MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT:
+ setActivateDisplayOnLiftWrist(builder);
+ break;
+ case MiBandConst.PREF_MIBAND_FITNESS_GOAL:
+ setFitnessGoal(builder);
+ break;
+ }
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+
+ @Override
+ public void onTestNewFunction() {
+ }
+
+ private MiBand2Support setDateDisplay(TransactionBuilder builder) {
+ DateTimeDisplay dateTimeDisplay = MiBand2Coordinator.getDateDisplay(getContext());
+ LOG.info("Setting date display to " + dateTimeDisplay);
+ switch (dateTimeDisplay) {
+ case TIME:
+ builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.DATEFORMAT_TIME);
+ break;
+ case DATE_TIME:
+ builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.DATEFORMAT_DATE_TIME);
+ break;
+ }
+ return this;
+ }
+
+ private MiBand2Support setActivateDisplayOnLiftWrist(TransactionBuilder builder) {
+ boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist();
+ LOG.info("Setting activate display on lift wrist to " + enable);
+ if (enable) {
+ builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST);
+ } else {
+ builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST);
+ }
+ return this;
+ }
+
+ public void phase2Initialize(TransactionBuilder builder) {
+ LOG.info("phase2Initialize...");
+ enableFurtherNotifications(builder, true);
+ setDateDisplay(builder);
+ setWearLocation(builder);
+ setFitnessGoal(builder);
+ setActivateDisplayOnLiftWrist(builder);
+ setHeartrateSleepSupport(builder);
+
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java
index 165e72e50..45fcc9fd5 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java
@@ -21,22 +21,32 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
-import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -67,10 +77,6 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FL
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_GENERIC;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_K9MAIL;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_PEBBLEMSG;
-import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_SMS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE;
@@ -88,13 +94,16 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
public static final boolean MI_1A_HR_FW_UPDATE_TEST_MODE_ENABLED = false;
private volatile boolean telephoneRinging;
private volatile boolean isLocatingDevice;
+ private volatile boolean isReadingSensorData;
private DeviceInfo mDeviceInfo;
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
+ private RealtimeSamplesSupport realtimeSamplesSupport;
public MiBandSupport() {
+ super(LOG);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE);
@@ -243,6 +252,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
static final byte[] startRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 1};
static final byte[] stopRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 0};
+ private static final byte[] startSensorRead = new byte[]{MiBandService.COMMAND_GET_SENSOR_DATA, 1};
+ private static final byte[] stopSensorRead = new byte[]{MiBandService.COMMAND_GET_SENSOR_DATA, 0};
+
/**
* Part of device initialization process. Do not call manually.
*
@@ -318,12 +330,21 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
* @return
*/
private MiBandSupport pair(TransactionBuilder transaction) {
- LOG.info("Attempting to pair MI device...");
- BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR);
- if (characteristic != null) {
- transaction.write(characteristic, new byte[]{2});
- } else {
- LOG.info("Unable to pair MI device -- characteristic not available");
+// this is apparently only needed to get a more strict bond between mobile and mi band,
+// e.g. such that Mi Fit and Gadgetbridge can coexist without needing to re-pair (with
+// full device-data-reset).
+// Unfortunately this extra pairing causes problems when bonding is not used/does not work
+// so we only do this when configured to keep data on the device
+
+ Prefs prefs = GBApplication.getPrefs();
+ if (prefs.getBoolean(MiBandConst.PREF_MIBAND_DONT_ACK_TRANSFER, false)) {
+ LOG.info("Attempting to pair MI device...");
+ BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR);
+ if (characteristic != null) {
+ transaction.write(characteristic, new byte[]{2});
+ } else {
+ LOG.info("Unable to pair MI device -- characteristic not available");
+ }
}
return this;
}
@@ -453,7 +474,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
int originalColour = getPreferredOriginalColour(notificationOrigin, prefs);
int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs);
+// setLowLatency(builder);
sendCustomNotification(profile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder);
+// setHighLatency(builder);
// sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder);
builder.queue(getQueue());
} catch (IOException ex) {
@@ -517,20 +540,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
@Override
public void onNotification(NotificationSpec notificationSpec) {
- // FIXME: these ORIGIN contants do not really make sense anymore
- switch (notificationSpec.type) {
- case SMS:
- performPreferredNotification("sms received", ORIGIN_SMS, null);
- break;
- case EMAIL:
- performPreferredNotification("email received", ORIGIN_K9MAIL, null);
- break;
- case CHAT:
- performPreferredNotification("chat message received", ORIGIN_PEBBLEMSG, null);
- break;
- default:
- performPreferredNotification("generic notification received", ORIGIN_GENERIC, null);
- }
+ String origin = notificationSpec.type.getGenericType();
+ performPreferredNotification(origin + " received", origin, null);
}
@Override
@@ -595,6 +606,10 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
}
}
+ @Override
+ public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
+ }
+
private boolean isTelephoneRinging() {
// don't synchronize, this is not really important
return telephoneRinging;
@@ -650,6 +665,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous);
}
builder.queue(getQueue());
+ enableRealtimeSamplesTimer(enable);
} catch (IOException ex) {
LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex);
}
@@ -675,6 +691,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
}
}
+ @Override
+ public void onSetConstantVibration(int intensity) {
+
+ }
+
@Override
public void onFetchActivityData() {
try {
@@ -695,6 +716,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications")
.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), enable ? getLowLatency() : getHighLatency())
.write(controlPoint, enable ? startRealTimeStepsNotifications : stopRealTimeStepsNotifications).queue(getQueue());
+ enableRealtimeSamplesTimer(enable);
} catch (IOException e) {
LOG.error("Unable to change realtime steps notification to: " + enable, e);
}
@@ -767,105 +789,121 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
// not supported
}
+ @Override
+ public void onAppReorder(UUID[] uuids) {
+ // not supported
+ }
+
@Override
public void onScreenshotReq() {
// not supported
}
@Override
- public void onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic) {
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
UUID characteristicUUID = characteristic.getUuid();
if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS);
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) {
handleNotificationNotif(characteristic.getValue());
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) {
handleRealtimeSteps(characteristic.getValue());
- } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) {
- handleRealtimeSteps(characteristic.getValue());
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) {
handleHeartrate(characteristic.getValue());
+ return true;
+ } else if (MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA.equals(characteristicUUID)) {
+ handleSensorData(characteristic.getValue());
} else {
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
logMessageContent(characteristic.getValue());
}
+ return false;
}
@Override
- public void onCharacteristicRead(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic, int status) {
+ public boolean onCharacteristicRead(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
UUID characteristicUUID = characteristic.getUuid();
if (MiBandService.UUID_CHARACTERISTIC_DEVICE_INFO.equals(characteristicUUID)) {
handleDeviceInfo(characteristic.getValue(), status);
+ return true;
} else if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) {
handleDeviceName(characteristic.getValue(), status);
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
handleBatteryInfo(characteristic.getValue(), status);
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) {
- logHeartrate(characteristic.getValue());
+ logHeartrate(characteristic.getValue(), status);
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_DATE_TIME.equals(characteristicUUID)) {
- logDate(characteristic.getValue());
+ logDate(characteristic.getValue(), status);
+ return true;
} else {
LOG.info("Unhandled characteristic read: " + characteristicUUID);
logMessageContent(characteristic.getValue());
}
+ return false;
}
@Override
- public void onCharacteristicWrite(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic, int status) {
+ public boolean onCharacteristicWrite(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
UUID characteristicUUID = characteristic.getUuid();
if (MiBandService.UUID_CHARACTERISTIC_PAIR.equals(characteristicUUID)) {
handlePairResult(characteristic.getValue(), status);
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_USER_INFO.equals(characteristicUUID)) {
handleUserInfoResult(characteristic.getValue(), status);
+ return true;
} else if (MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT.equals(characteristicUUID)) {
handleControlPointResult(characteristic.getValue(), status);
+ return true;
}
+ return false;
}
- /**
- * Utility method that may be used to log incoming messages when we don't know how to deal with them yet.
- *
- * @param value
- */
- public void logMessageContent(byte[] value) {
- LOG.info("RECEIVED DATA WITH LENGTH: " + value.length);
- for (byte b : value) {
- LOG.warn("DATA: " + String.format("0x%2x", b));
- }
- }
-
- public void logDate(byte[] value) {
- GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value);
- LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime()));
- }
-
- public void logHeartrate(byte[] value) {
- LOG.info("Got heartrate:");
- if (value.length == 2 && value[0] == 6) {
- int hrValue = (value[1] & 0xff);
- GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO);
+ public void logDate(byte[] value, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value);
+ LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime()));
} else {
logMessageContent(value);
}
}
+ public void logHeartrate(byte[] value, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS && value != null) {
+ LOG.info("Got heartrate:");
+ if (value.length == 2 && value[0] == 6) {
+ int hrValue = (value[1] & 0xff);
+ GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO);
+ }
+ return;
+ }
+ logMessageContent(value);
+ }
+
private void handleHeartrate(byte[] value) {
if (value.length == 2 && value[0] == 6) {
int hrValue = (value[1] & 0xff);
if (LOG.isDebugEnabled()) {
LOG.debug("heart rate: " + hrValue);
}
- Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT)
- .putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, hrValue)
- .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
- LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport();
+ realtimeSamplesSupport.setHeartrateBpm(hrValue);
+ if (!realtimeSamplesSupport.isRunning()) {
+ // single shot measurement, manually invoke storage and result publishing
+ realtimeSamplesSupport.triggerCurrentSample();
+ }
}
}
@@ -874,10 +912,75 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
if (LOG.isDebugEnabled()) {
LOG.debug("realtime steps: " + steps);
}
- Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS)
- .putExtra(DeviceService.EXTRA_REALTIME_STEPS, steps)
- .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
- LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ getRealtimeSamplesSupport().setSteps(steps);
+ }
+
+ private void enableRealtimeSamplesTimer(boolean enable) {
+ if (enable) {
+ getRealtimeSamplesSupport().start();
+ } else {
+ if (realtimeSamplesSupport != null) {
+ realtimeSamplesSupport.stop();
+ }
+ }
+ }
+
+ public MiBandActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) {
+ MiBandActivitySample sample = new MiBandActivitySample();
+ sample.setDevice(device);
+ sample.setUser(user);
+ sample.setTimestamp(timestampInSeconds);
+ sample.setProvider(provider);
+
+ return sample;
+ }
+
+ private RealtimeSamplesSupport getRealtimeSamplesSupport() {
+ if (realtimeSamplesSupport == null) {
+ realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) {
+ @Override
+ public void doCurrentSample() {
+
+ try (DBHandler handler = GBApplication.acquireDB()) {
+ DaoSession session = handler.getDaoSession();
+
+ Device device = DBHelper.getDevice(getDevice(), session);
+ User user = DBHelper.getUser(session);
+ int ts = (int) (System.currentTimeMillis() / 1000);
+ MiBandSampleProvider provider = new MiBandSampleProvider(gbDevice, session);
+ MiBandActivitySample sample = createActivitySample(device, user, ts, provider);
+ sample.setHeartRate(getHeartrateBpm());
+ sample.setSteps(getSteps());
+ sample.setRawIntensity(ActivitySample.NOT_MEASURED);
+ sample.setRawKind(MiBandSampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that?
+
+ // TODO: remove this once fully ported to REALTIME_SAMPLES
+ if (sample.getSteps() != ActivitySample.NOT_MEASURED) {
+ Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS)
+ .putExtra(DeviceService.EXTRA_REALTIME_STEPS, sample.getSteps())
+ .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ }
+ if (sample.getHeartRate() != ActivitySample.NOT_MEASURED) {
+ Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT)
+ .putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, sample.getHeartRate())
+ .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ }
+
+// Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
+// .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
+// LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+
+ LOG.debug("Storing realtime sample: " + sample);
+ provider.addGBActivitySample(sample);
+ } catch (Exception e) {
+ LOG.warn("Unable to acquire db for saving realtime samples", e);
+ }
+ }
+ };
+ }
+ return realtimeSamplesSupport;
}
/**
@@ -921,6 +1024,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
getDevice().sendDeviceUpdateIntent(getContext());
break;
+ case MiBandService.NOTIFY_SET_LATENCY_SUCCESS:
+ LOG.info("Setting latency succeeded.");
+ break;
default:
for (byte b : value) {
LOG.warn("DATA: " + String.format("0x%2x", b));
@@ -942,9 +1048,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
mDeviceInfo = new DeviceInfo(value);
mDeviceInfo.setTest1AHRMode(MI_1A_HR_FW_UPDATE_TEST_MODE_ENABLED);
if (getDeviceInfo().supportsHeartrate()) {
- getDevice().addDeviceInfo(new GenericItem(
- getContext().getString(R.string.DEVINFO_HR_VER),
- MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getHeartrateFirmwareVersion())));
+ getDevice().setFirmwareVersion2(MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getHeartrateFirmwareVersion()));
}
LOG.warn("Device info: " + mDeviceInfo);
versionCmd.hwVersion = mDeviceInfo.getHwVersion();
@@ -1074,22 +1178,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
int slotToUse = 2 - iteration;
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(mEvt.getBegin());
- byte[] calBytes = MiBandDateConverter.calendarToRawBytes(calendar);
-
- byte[] alarmMessage = new byte[]{
- MiBandService.COMMAND_SET_TIMER,
- (byte) slotToUse,
- (byte) 1,
- calBytes[0],
- calBytes[1],
- calBytes[2],
- calBytes[3],
- calBytes[4],
- calBytes[5],
- (byte) 0,
- (byte) 0
- };
- builder.write(characteristic, alarmMessage);
+ Alarm alarm = GBAlarm.createSingleShot(slotToUse, false, calendar);
+ queueAlarm(alarm, builder, characteristic);
iteration++;
}
builder.queue(getQueue());
@@ -1099,5 +1189,45 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
}
}
+ @Override
+ public void onSendConfiguration(String config) {
+ // nothing yet
+ }
+ @Override
+ public void onTestNewFunction() {
+ try {
+ TransactionBuilder builder = performInitialized("Toggle sensor reading");
+ if (isReadingSensorData) {
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), stopSensorRead);
+ isReadingSensorData = false;
+ } else {
+ builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), startSensorRead);
+ isReadingSensorData = true;
+ }
+ builder.queue(getQueue());
+ } catch (IOException ex) {
+ LOG.error("Unable to toggle sensor reading MI", ex);
+ }
+ }
+
+ private void handleSensorData(byte[] value) {
+ int counter=0, step=0, axis1=0, axis2=0, axis3 =0;
+ if ((value.length - 2) % 6 != 0) {
+ LOG.warn("GOT UNEXPECTED SENSOR DATA WITH LENGTH: " + value.length);
+ for (byte b : value) {
+ LOG.warn("DATA: " + String.format("0x%4x", b));
+ }
+ }
+ else {
+ counter = (value[0] & 0xff) | ((value[1] & 0xff) << 8);
+ for (int idx = 0; idx < ((value.length - 2) / 6); idx++) {
+ step = idx * 6;
+ axis1 = (value[step+2] & 0xff) | ((value[step+3] & 0xff) << 8);
+ axis2 = (value[step+4] & 0xff) | ((value[step+5] & 0xff) << 8);
+ axis3 = (value[step+6] & 0xff) | ((value[step+7] & 0xff) << 8);
+ }
+ LOG.info("READ SENSOR DATA VALUES: counter:"+counter+" step:"+step+" axis1:"+axis1+" axis2:"+axis2+" axis3:"+axis3+";");
+ }
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java
new file mode 100644
index 000000000..a160af8c6
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java
@@ -0,0 +1,86 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+
+/**
+ * Basic support for aggregating different sources of realtime data that comes in in a mostly
+ * fixed interval. The aggregated data will be stored together.
+ *
+ * start() and stop() may be called multiple times, but the first stop() call will really
+ * stop the timer.
+ * manner.
+ *
+ * Subclasses must implement #doCurrentSample() and should override #resetCurrentValues()
+ * (but call super!).
+ */
+public abstract class RealtimeSamplesSupport {
+ private final long delay;
+ private final long period;
+
+ protected int steps;
+ protected int heartrateBpm;
+ // subclasses may add more
+
+ private Timer realtimeStorageTimer;
+
+ public RealtimeSamplesSupport(long delay, long period) {
+ this.delay = delay;
+ this.period = period;
+ }
+
+ public synchronized void start() {
+ if (isRunning()) {
+ return; // already running
+ }
+ realtimeStorageTimer = new Timer("Mi Band Realtime Storage Timer");
+ realtimeStorageTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ triggerCurrentSample();
+ }
+ }, delay, period);
+ }
+
+ public synchronized void stop() {
+ if (realtimeStorageTimer != null) {
+ realtimeStorageTimer.cancel();
+ realtimeStorageTimer.purge();
+ realtimeStorageTimer = null;
+ }
+ }
+
+ public synchronized boolean isRunning() {
+ return realtimeStorageTimer != null;
+ }
+
+ public void setSteps(int stepsPerMinute) {
+ this.steps = stepsPerMinute;
+ }
+
+ public int getSteps() {
+ return steps;
+ }
+
+ public void setHeartrateBpm(int hrBpm) {
+ this.heartrateBpm = hrBpm;
+ }
+
+ public int getHeartrateBpm() {
+ return heartrateBpm;
+ }
+
+ public void triggerCurrentSample() {
+ doCurrentSample();
+ resetCurrentValues();
+ }
+
+ protected void resetCurrentValues() {
+ steps = ActivitySample.NOT_MEASURED;
+ heartrateBpm = ActivitySample.NOT_MEASURED;
+ }
+
+ protected abstract void doCurrentSample();
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java
index 132ec182b..ab28be7b7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java
@@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
@@ -16,9 +17,9 @@ public class V1NotificationStrategy implements NotificationStrategy {
static final byte[] startVibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, 1};
static final byte[] stopVibrate = new byte[]{MiBandService.COMMAND_STOP_MOTOR_VIBRATE};
- private final MiBandSupport support;
+ private final AbstractBTLEDeviceSupport support;
- public V1NotificationStrategy(MiBandSupport support) {
+ public V1NotificationStrategy(AbstractBTLEDeviceSupport support) {
this.support = support;
}
@@ -61,6 +62,7 @@ public class V1NotificationStrategy implements NotificationStrategy {
* @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example.
* @param builder
*/
+ @Override
public void sendCustomNotification(VibrationProfile vibrationProfile, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) {
BluetoothGattCharacteristic controlPoint = support.getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT);
for (short i = 0; i < vibrationProfile.getRepeat(); i++) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java
index 32ed1275f..8d2135433 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java
@@ -3,24 +3,29 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
import android.bluetooth.BluetoothGattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
public class V2NotificationStrategy implements NotificationStrategy {
- private final MiBandSupport support;
+ private final AbstractBTLEDeviceSupport support;
- public V2NotificationStrategy(MiBandSupport support) {
+ public V2NotificationStrategy(AbstractBTLEDeviceSupport support) {
this.support = support;
}
+ protected AbstractBTLEDeviceSupport getSupport() {
+ return support;
+ }
+
@Override
public void sendDefaultNotification(TransactionBuilder builder, BtLEAction extraAction) {
VibrationProfile profile = VibrationProfile.getProfile(VibrationProfile.ID_MEDIUM, (short) 3);
sendCustomNotification(profile, extraAction, builder);
}
- private void sendCustomNotification(VibrationProfile vibrationProfile, BtLEAction extraAction, TransactionBuilder builder) {
+ protected void sendCustomNotification(VibrationProfile vibrationProfile, BtLEAction extraAction, TransactionBuilder builder) {
//use the new alert characteristic
BluetoothGattCharacteristic alert = support.getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL);
for (short i = 0; i < vibrationProfile.getRepeat(); i++) {
@@ -29,6 +34,8 @@ public class V2NotificationStrategy implements NotificationStrategy {
int on = onOffSequence[j];
on = Math.min(500, on); // longer than 500ms is not possible
builder.write(alert, new byte[]{GattCharacteristic.MILD_ALERT}); //MILD_ALERT lights up GREEN leds, HIGH_ALERT lights up RED leds
+// builder.wait(on);
+// builder.write(alert, new byte[]{GattCharacteristic.HIGH_ALERT});
builder.wait(on);
builder.write(alert, new byte[]{GattCharacteristic.NO_ALERT});
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java
new file mode 100644
index 000000000..00c1d7baa
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java
@@ -0,0 +1,17 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
+
+public abstract class AbstractMiBand1Operation extends AbstractMiBandOperation {
+ protected AbstractMiBand1Operation(MiBandSupport support) {
+ super(support);
+ }
+
+ @Override
+ protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) {
+ builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
+ .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java
index 2f6577d7f..f0b143e3d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java
@@ -4,22 +4,23 @@ import android.widget.Toast;
import java.io.IOException;
-import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
-public abstract class AbstractMiBandOperation extends AbstractBTLEOperation {
- protected AbstractMiBandOperation(MiBandSupport support) {
+public abstract class AbstractMiBandOperation extends AbstractBTLEOperation {
+ protected AbstractMiBandOperation(T support) {
super(support);
}
@Override
protected void prePerform() throws IOException {
super.prePerform();
+ getDevice().setBusyTask("fetch activity data"); // mark as busy quickly to avoid interruptions from the outside
TransactionBuilder builder = performInitialized("disabling some notifications");
enableOtherNotifications(builder, false);
+ enableNeededNotifications(builder, true);
builder.queue(getQueue());
}
@@ -27,9 +28,10 @@ public abstract class AbstractMiBandOperation extends AbstractBTLEOperation {
+ protected AbstractMiBand2Operation(MiBand2Support support) {
+ super(support);
+ }
+
+ @Override
+ protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) {
+ // TODO: check which notifications we should disable and re-enable here
+// builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
+// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/Mi2NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/Mi2NotificationStrategy.java
new file mode 100644
index 000000000..3cfed0a92
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/Mi2NotificationStrategy.java
@@ -0,0 +1,48 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.V2NotificationStrategy;
+
+public class Mi2NotificationStrategy extends V2NotificationStrategy {
+
+ public Mi2NotificationStrategy(AbstractBTLEDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ protected void sendCustomNotification(VibrationProfile vibrationProfile, BtLEAction extraAction, TransactionBuilder builder) {
+ //use the new alert characteristic
+ BluetoothGattCharacteristic alert = getSupport().getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL);
+ for (short i = 0; i < vibrationProfile.getRepeat(); i++) {
+ int[] onOffSequence = vibrationProfile.getOnOffSequence();
+ for (int j = 0; j < onOffSequence.length; j++) {
+ int on = onOffSequence[j];
+ on = Math.min(500, on); // longer than 500ms is not possible
+ builder.write(alert, new byte[]{(byte) vibrationProfile.getAlertLevel()});
+ builder.wait(on);
+ builder.write(alert, new byte[]{GattCharacteristic.NO_ALERT});
+
+ if (++j < onOffSequence.length) {
+ int off = Math.max(onOffSequence[j], 25); // wait at least 25ms
+ builder.wait(off);
+ }
+
+ if (extraAction != null) {
+ builder.add(extraAction);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void sendCustomNotification(VibrationProfile vibrationProfile, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) {
+ // all other parameters are unfortunately not supported anymore ;-(
+ sendCustomNotification(vibrationProfile, extraAction, builder);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java
new file mode 100644
index 000000000..881e5ca59
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java
@@ -0,0 +1,248 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.Logging;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation;
+import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+/**
+ * An operation that fetches activity data. For every fetch, a new operation must
+ * be created, i.e. an operation may not be reused for multiple fetches.
+ */
+public class FetchActivityOperation extends AbstractMiBand2Operation {
+ private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class);
+
+ private List samples = new ArrayList<>(60*24); // 1day per default
+
+ private byte lastPacketCounter = -1;
+ private Calendar startTimestamp;
+
+ public FetchActivityOperation(MiBand2Support support) {
+ super(support);
+ }
+
+ @Override
+ protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
+ if (!enable) {
+ // dynamically enabled, but always disabled on finish
+ builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable);
+ }
+ }
+
+ @Override
+ protected void doPerform() throws IOException {
+ TransactionBuilder builder = performInitialized("fetching activity data");
+ getSupport().setLowLatency(builder);
+ builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
+ BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4);
+ builder.notify(characteristicFetch, true);
+ BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA);
+
+ GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime();
+ builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES)));
+ builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply
+ builder.notify(characteristicActivityData, true);
+ builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_ACTIVITY_DATA });
+ builder.queue(getQueue());
+ }
+
+ private GregorianCalendar getLastSuccessfulSynchronizedTime() {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DaoSession session = dbHandler.getDaoSession();
+ SampleProvider sampleProvider = new MiBand2SampleProvider(getDevice(), session);
+ MiBandActivitySample sample = sampleProvider.getLatestActivitySample();
+ if (sample != null) {
+ int timestamp = sample.getTimestamp();
+ GregorianCalendar calendar = BLETypeConversions.createCalendar();
+ calendar.setTimeInMillis((long) timestamp * 1000);
+ return calendar;
+ }
+ } catch (Exception ex) {
+ LOG.error("Error querying for latest activity sample, synchronizing the last 10 days", ex);
+ }
+
+ GregorianCalendar calendar = BLETypeConversions.createCalendar();
+ calendar.add(Calendar.DAY_OF_MONTH, -10);
+ return calendar;
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ UUID characteristicUUID = characteristic.getUuid();
+ if (MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) {
+ handleActivityNotif(characteristic.getValue());
+ return true;
+ } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
+ handleActivityMetadata(characteristic.getValue());
+ return true;
+ } else {
+ return super.onCharacteristicChanged(gatt, characteristic);
+ }
+ }
+
+ private void handleActivityFetchFinish() {
+ LOG.info("Fetching activity data has finished.");
+ saveSamples();
+ operationFinished();
+ unsetBusy();
+ }
+
+ private void saveSamples() {
+ if (samples.size() > 0) {
+ // save all the samples that we got
+ try (DBHandler handler = GBApplication.acquireDB()) {
+ DaoSession session = handler.getDaoSession();
+ SampleProvider sampleProvider = new MiBandSampleProvider(getDevice(), session);
+ Device device = DBHelper.getDevice(getDevice(), session);
+ User user = DBHelper.getUser(session);
+
+ GregorianCalendar timestamp = (GregorianCalendar) startTimestamp.clone();
+ for (MiBandActivitySample sample : samples) {
+ sample.setDevice(device);
+ sample.setUser(user);
+ sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000));
+ sample.setProvider(sampleProvider);
+
+ if (LOG.isDebugEnabled()) {
+// LOG.debug("sample: " + sample);
+ }
+
+ timestamp.add(Calendar.MINUTE, 1);
+ }
+ sampleProvider.addGBActivitySamples(samples.toArray(new MiBandActivitySample[0]));
+
+ LOG.info("Mi2 activity data: last sample timestamp: " + DateTimeUtils.formatDateTime(timestamp.getTime()));
+
+ } catch (Exception ex) {
+ GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR);
+ } finally {
+ samples.clear();
+ }
+ }
+ }
+
+ /**
+ * Method to handle the incoming activity data.
+ * There are two kind of messages we currently know:
+ * - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
+ * - the second one is 20 bytes long and contains the actual activity data
+ *
+ * The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
+ *
+ * @param value
+ */
+ private void handleActivityNotif(byte[] value) {
+ if (!isOperationRunning()) {
+ LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length);
+ getSupport().logMessageContent(value);
+ return;
+ }
+
+ if ((value.length % 4) == 1) {
+ if ((byte) (lastPacketCounter + 1) == value[0] ) {
+ lastPacketCounter++;
+ bufferActivityData(value);
+ } else {
+ GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR);
+ handleActivityFetchFinish();
+ return;
+ }
+ } else {
+ GB.toast("Error fetching activity data, unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR);
+ }
+ }
+
+ /**
+ * Creates samples from the given 17-length array
+ * @param value
+ */
+ private void bufferActivityData(byte[] value) {
+ int len = value.length;
+
+ if (len % 4 != 1) {
+ throw new AssertionError("Unexpected activity array size: " + value);
+ }
+
+ for (int i = 1; i < len; i+=4) {
+ MiBandActivitySample sample = createSample(value[i], value[i + 1], value[i + 2], value[i + 3]);
+ samples.add(sample);
+ }
+ }
+
+ private MiBandActivitySample createSample(byte category, byte intensity, byte steps, byte heartrate) {
+ MiBandActivitySample sample = new MiBandActivitySample();
+ sample.setRawKind(category & 0xff);
+ sample.setRawIntensity(intensity & 0xff);
+ sample.setSteps(steps & 0xff);
+ sample.setHeartRate(heartrate & 0xff);
+
+ return sample;
+ }
+
+ private void handleActivityMetadata(byte[] value) {
+ if (value.length == 15) {
+ // first two bytes are whether our request was accepted
+ if (ArrayUtils.equals(MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, value, 0, 2)) {
+ // the third byte (0x01 on success) = ?
+ // the 4th - 7th bytes probably somehow represent the number of bytes/packets to expect
+
+ // last 8 bytes are the start date
+ Calendar startTimestamp = getSupport().fromTimeBytes(org.apache.commons.lang3.ArrayUtils.subarray(value, 7, value.length));
+ setStartTimestamp(startTimestamp);
+
+ GB.toast(getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
+ DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), Toast.LENGTH_LONG, GB.INFO);
+ } else {
+ LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value));
+ handleActivityFetchFinish();
+ }
+ } else if (value.length == 3) {
+ if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) {
+ handleActivityFetchFinish();
+ } else {
+ LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value));
+ handleActivityFetchFinish();
+ }
+ }
+ }
+
+ private void setStartTimestamp(Calendar startTimestamp) {
+ this.startTimestamp = startTimestamp;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java
new file mode 100644
index 000000000..5adabc67e
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java
@@ -0,0 +1,139 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.UUID;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.SecretKeySpec;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class InitOperation extends AbstractBTLEOperation {
+ private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class);
+
+ private final TransactionBuilder builder;
+ private final boolean needsAuth;
+
+ public InitOperation(boolean needsAuth, MiBand2Support support, TransactionBuilder builder) {
+ super(support);
+ this.needsAuth = needsAuth;
+ this.builder = builder;
+ builder.setGattCallback(this);
+ }
+
+ @Override
+ protected void doPerform() throws IOException {
+ getSupport().enableNotifications(builder, true);
+ if (needsAuth) {
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
+ // write key to miband2
+ byte[] sendKey = org.apache.commons.lang3.ArrayUtils.addAll(new byte[]{MiBand2Service.AUTH_SEND_KEY, MiBand2Service.AUTH_BYTE}, getSecretKey());
+ builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), sendKey);
+ } else {
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
+ // get random auth number
+ builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), requestAuthNumber());
+ }
+ }
+
+ private byte[] requestAuthNumber() {
+ return new byte[]{MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER, MiBand2Service.AUTH_BYTE};
+ }
+
+ private byte[] getSecretKey() {
+ return new byte[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45};
+ }
+
+ @Override
+ public TransactionBuilder performInitialized(String taskName) throws IOException {
+ throw new UnsupportedOperationException("This IS the initialization class, you cannot call this method");
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ UUID characteristicUUID = characteristic.getUuid();
+ if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) {
+ try {
+ byte[] value = characteristic.getValue();
+ getSupport().logMessageContent(value);
+ if (value[0] == MiBand2Service.AUTH_RESPONSE &&
+ value[1] == MiBand2Service.AUTH_SEND_KEY &&
+ value[2] == MiBand2Service.AUTH_SUCCESS) {
+ TransactionBuilder builder = createTransactionBuilder("Sending the secret key to the band");
+ builder.write(characteristic, requestAuthNumber());
+ getSupport().performImmediately(builder);
+ } else if (value[0] == MiBand2Service.AUTH_RESPONSE &&
+ value[1] == MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER &&
+ value[2] == MiBand2Service.AUTH_SUCCESS) {
+ // md5??
+ byte[] eValue = handleAESAuth(value, getSecretKey());
+ byte[] responseValue = org.apache.commons.lang3.ArrayUtils.addAll(
+ new byte[]{MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER, MiBand2Service.AUTH_BYTE}, eValue);
+
+ TransactionBuilder builder = createTransactionBuilder("Sending the encrypted random key to the band");
+ builder.write(characteristic, responseValue);
+ getSupport().setCurrentTimeWithService(builder);
+ getSupport().performImmediately(builder);
+ } else if (value[0] == MiBand2Service.AUTH_RESPONSE &&
+ value[1] == MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER &&
+ value[2] == MiBand2Service.AUTH_SUCCESS) {
+ TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2");
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
+ getSupport().requestDeviceInfo(builder);
+ getSupport().phase2Initialize(builder);
+ getSupport().setInitialized(builder);
+ getSupport().performImmediately(builder);
+ } else {
+ return super.onCharacteristicChanged(gatt, characteristic);
+ }
+ } catch (Exception e) {
+ GB.toast(getContext(), "Error authenticating Mi Band 2", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ return true;
+ } else {
+ LOG.info("Unhandled characteristic changed: " + characteristicUUID);
+ return super.onCharacteristicChanged(gatt, characteristic);
+ }
+ }
+
+ private TransactionBuilder createTransactionBuilder(String task) {
+ TransactionBuilder builder = getSupport().createTransactionBuilder(task);
+ builder.setGattCallback(this);
+ return builder;
+ }
+
+ private byte[] getMD5(byte[] message) throws NoSuchAlgorithmException {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return md5.digest(message);
+ }
+
+ private byte[] handleAESAuth(byte[] value, byte[] secretKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException {
+ byte[] mValue = Arrays.copyOfRange(value, 3, 19);
+ Cipher ecipher = Cipher.getInstance("AES/ECB/NoPadding");
+ SecretKeySpec newKey = new SecretKeySpec(secretKey, "AES");
+ ecipher.init(Cipher.ENCRYPT_MODE, newKey);
+ byte[] enc = ecipher.doFinal(mValue);
+ return enc;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java
index 710178f9f..1944f2d5f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java
@@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class AppMessageHandler {
protected final PebbleProtocol mPebbleProtocol;
@@ -32,4 +33,8 @@ public class AppMessageHandler {
public GBDeviceEvent[] pushMessage() {
return null;
}
+
+ protected GBDevice getDevice() {
+ return mPebbleProtocol.getDevice();
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java
deleted file mode 100644
index f3ac052fa..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
-
-import android.util.Pair;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayList;
-import java.util.SimpleTimeZone;
-import java.util.TimeZone;
-import java.util.UUID;
-
-import nodomain.freeyourgadget.gadgetbridge.GBApplication;
-import nodomain.freeyourgadget.gadgetbridge.GBException;
-import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
-import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
-import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-
-public class AppMessageHandlerGBPebble extends AppMessageHandler {
-
- public static final int KEY_TIMESTAMP = 1;
- public static final int KEY_SAMPLES = 2;
-
- private static final Logger LOG = LoggerFactory.getLogger(AppMessageHandlerGBPebble.class);
-
- AppMessageHandlerGBPebble(UUID uuid, PebbleProtocol pebbleProtocol) {
- super(uuid, pebbleProtocol);
- }
-
- @Override
- public GBDeviceEvent[] handleMessage(ArrayList> pairs) {
- int timestamp = 0;
- for (Pair