Merge branch 'master' into new_GUI

# Conflicts:
#	app/build.gradle
master
Daniele Gobbetti 2017-02-26 16:55:54 +01:00
commit 176cf79cc1
170 changed files with 7896 additions and 2323 deletions

10
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,10 @@
#### Your issue is:
*In case of a bug, do not forget to attach logs!*
#### Your wearable device is:
*Please specify model and firmware version if possible*
#### Your android version is:
#### Your Gadgetbridge version is:

View File

@ -1,5 +1,77 @@
###Changelog
####Version 0.17.5
* Automatically start the service on boot (can be turned off)
* Pebble: PebbleKit compatibility improvements (Datalogging)
* Pebble: Display music shuffle and repeat states for some players
* Pebble 2/LE: Speed up data transfer
####Version 0.17.4
* Better integration with android music players
* Privacy options for calls (hide caller name/number)
* Send a notification to the connected if the Android Alarm Clock rings (com.android.deskclock)
* Fixes for cyrillic transliteration
* Pebble: Implement notification privacy modes
* Pebble: Support weather for Obisdian watchface
* Pebble: add a dev option to always and immediately ACK PebbleKit messages to the watch
* HPlus: Support alarms
* HPlus: Fix time and date sync and time format (12/24)
* HPlus: Add device specific preferences and icon
* HPlus: Support for Makibes F68
####Version 0.17.3
* HPlus: Improve display of new messages and phone calls
* HPlus: Fix bug related to steps and heart rate
* Pebble: Support dynamic keys for natively supported watchfaces and watchapps (more stability accross versions)
* Pebble: Fix error Toast being displayed when TimeStyle watchface is not installed
* Mi Band 1+2: Support for connecting wihout BT pairing (workaround for certain connection problems)
####Version 0.17.2
* Pebble: Fix temperature unit in Timestyle Pebble watchface
* Add optional Cyrillic transliteration (for devices lacking the font)
####Version 0.17.1
* Pebble: Fix installation of some watchapps
* Pebble: Try to improve PebbleKit compatibility
* HPlus: Fix bug setting current date
####Version 0.17.0
* Add weather support through "Weather Notification" app
* Various fixes for K9 mail when using the generic notification receiver
* Add a preference to hide the persistent notification icon of Gadgetbridge
* Pebble: Support for build-in weather system app (FW 4.x)
* Pebble: Add weather support for various watchfaces
* Pebble: Add option to disable call display
* Pebble: Add option to automatically delete notifications that got dismissed on the phone
* Pebble: Bugfix for some PebbleKit enabled 3rd party apps (TCW and maybe other)
* Pebble 2/LE: Improve reliablitly and transfer speed
* HPlus: Improved discovery and pairing
* HPlus: Improved notifications (display + vibration)
* HPlus: Synchronize time and date
* HPlus: Display firmware version and battery charge
* HPlus: Near real time Heart rate measurement
* HPlus: Experimental synchronization of activity data (only sleep, steps and intensity)
* HPlus: Fix some disconnection issues
####Version 0.16.0
* New devices: HPlus (e.g. Zeblaze ZeBand), contributed by João Paulo Barraca
* ZeBand: Initial support: notifications, heart rate, sleep monitoring, user configuration, date+time
* Pebble 2: Fix Pebble Classic FW 3.x app variant being prioritized over native Pebble 2 app variant
* Charts (Live Activity): Fix axis labels color in dark theme
* Mi Band: Fix ginormous step count when using Live Activity
* Mi Band: Improved performance during activity sync
* Mi Band 2: Fix activity data missing after doing manual hr measurements or live activity
* Support sharing firmwares/watchapps/watchfaces to Gadgetbridge
* Support for the "Subsonic" music player (#474)
####Version 0.15.2
* Mi Band: Fix crash with unknown notification sources
####Version 0.15.1
* Improved handling of notifications for some apps
* Pebble 2/LE: Add setting to limit GATT MTU for debugging broken BLE stacks
* Mi Band 2: Display battery status
####Version 0.15.0
* New device: Liveview
* Liveview: initial support (set the time and receive notifications)

View File

@ -1,28 +0,0 @@
Andreas Shimokawa <shimokawa@fsfe.org>
cpfeiffer <cpfeiffer@users.noreply.github.com>
Daniele Gobbetti <daniele+github@gobbetti.name>
Daniele Gobbetti <daniele@gobbetti.name>
danielegobbetti <daniele+github@gobbetti.name>
Carsten Pfeiffer <cpfeiffer@users.noreply.github.com>
Julien Pivotto <roidelapluie@inuits.eu>
Lem Dulfo <lemuel.dulfo@gmail.com>
Sergey Trofimov <sarg@sarg.org.ru>
Daniele Gobbetti <daniele.gobbetti@gmail.com>
cpfeiffer <cpfeiffer@users.github.com>
0nse <0nse@users.noreply.github.com>
Christian Fischer <sw-dev@computerlyrik.de>
Normano64 <per.bergqwist@gmail.com>
Ⲇⲁⲛⲓ Φi <daniphii@outlook.com>
xphnx <xphnx@users.noreply.github.com>
Tarik Sekmen <tarik@ilixi.org>
rober <rober@prtl.nodomain.net>
Nicolò Balzarotti <anothersms@gmail.com>
Marc Schlaich <marc.schlaich@googlemail.com>
kevlarcade <kevlarcade@gmail.com>
Kasha <kasha_malaga@hotmail.com>
Chris Perelstein <chris.perelstein@gmail.com>
Alexey Afanasev <avafanasiev@gmail.com>
And all the Transifex translators, which I cannot automatically list, at the moment.
git log --raw | grep "^Author: " | sort | uniq -c | sort -k 1 -n -r | cut -f 2- -d: > CONTRIBUTORS.md

68
CONTRIBUTORS.rst Normal file
View File

@ -0,0 +1,68 @@
.. 2>/dev/null
names ()
{
echo -e "\n exit;\n**Contributors (sorted by number of commits):**\n";
git log --format='%aN:%aE' origin/master | sed 's/@users.github.com/@users.noreply.github.com/g' | awk 'BEGIN{FS=":"}{ct[$2]+=1;if (length($1) > length(e[$2])) {e[$2]=$1}}END{for (i in e) { n[e[i]]=i;c[e[i]]+=ct[i] }; for (a in n) print c[a]"\t* "a" <"n[a]">";}' | sort -n -r | cut -f 2-
}
quine ()
{
{
echo ".. 2>/dev/null";
declare -f names | sed -e 's/^[[:space:]]*/ /';
declare -f quine | sed -e 's/^[[:space:]]*/ /';
echo -e " quine\n";
names;
echo -e "\nAnd all the Transifex translators, which I cannot automatically list, at the moment.\n\n*To update the contributors list just run this file with bash*"
} > CONTRIBUTORS.rst;
exit
}
quine
exit;
**Contributors (sorted by number of commits):**
* Andreas Shimokawa <shimokawa@fsfe.org>
* Carsten Pfeiffer <cpfeiffer@users.noreply.github.com>
* Daniele Gobbetti <daniele+github@gobbetti.name>
* João Paulo Barraca <jpbarraca@gmail.com>
* ivanovlev <lion.ivanov@gmal.com>
* Julien Pivotto <roidelapluie@inuits.eu>
* Steffen Liebergeld <perl@gmx.org>
* Lem Dulfo <lemuel.dulfo@gmail.com>
* Sergey Trofimov <sarg@sarg.org.ru>
* JohnnySun <bmy001@gmail.com>
* Uwe Hermann <uwe@hermann-uwe.de>
* 0nse <0nse@users.noreply.github.com>
* Gergely Peidl <gergely@peidl.net>
* Christian Fischer <sw-dev@computerlyrik.de>
* 6arms1leg <m.brnsfld@googlemail.com>
* Normano64 <per.bergqwist@gmail.com>
* Avamander <Avamander@users.noreply.github.com>
* Ⲇⲁⲛⲓ Φi <daniphii@outlook.com>
* Yar <yaroslav.isakov@gmail.com>
* xzovy <caleb@caleb-cooper.net>
* xphnx <xphnx@users.noreply.github.com>
* Tarik Sekmen <tarik@ilixi.org>
* Szymon Tomasz Stefanek <s.stefanek@gmail.com>
* Roman Plevka <rplevka@redhat.com>
* rober <rober@prtl.nodomain.net>
* Nicolò Balzarotti <anothersms@gmail.com>
* Natanael Arndt <arndtn@gmail.com>
* Marc Schlaich <marc.schlaich@googlemail.com>
* kevlarcade <kevlarcade@gmail.com>
* Kevin Richter <me@kevinrichter.nl>
* Kasha <kasha_malaga@hotmail.com>
* Ivan <ivan_tizhanin@mail.ru>
* Hasan Ammar <ammarh@gmail.com>
* Gilles MOREL <contact@gilles-morel.fr>
* Gilles Émilien MOREL <Almtesh@users.noreply.github.com>
* Chris Perelstein <chris.perelstein@gmail.com>
* Carlos Ferreira <calbertoferreira@gmail.com>
* atkyritsis <at.kyritsis@gmail.com>
* andre <andre.buesgen@yahoo.de>
* Alexey Afanasev <avafanasiev@gmail.com>
And all the Transifex translators, which I cannot automatically list, at the moment.
*To update the contributors list just run this file with bash*

View File

@ -60,6 +60,8 @@ public class GBDaoGenerator {
addPebbleHealthActivityKindOverlay(schema, user, device);
addPebbleMisfitActivitySample(schema, user, device);
addPebbleMorpheuzActivitySample(schema, user, device);
addHPlusHealthActivityKindOverlay(schema, user, device);
addHPlusHealthActivitySample(schema, user, device);
new DaoGenerator().generateAll(schema, "app/src/main/java");
}
@ -221,6 +223,36 @@ public class GBDaoGenerator {
return activitySample;
}
private static Entity addHPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "HPlusHealthActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addByteArrayProperty("rawHPlusHealthData");
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
activitySample.addIntProperty("distance");
activitySample.addIntProperty("calories");
return activitySample;
}
private static Entity addHPlusHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
Entity activityOverlay = addEntity(schema, "HPlusHealthActivityOverlay");
activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey();
activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey();
activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty();
activityOverlay.addToOne(device, deviceId);
Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty();
activityOverlay.addToOne(user, userId);
activityOverlay.addByteArrayProperty("rawHPlusHealthData");
return activityOverlay;
}
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");

View File

@ -20,6 +20,7 @@ need to create an account and transmit any of your data to the vendor's servers.
* Mi Band 2
* Vibratissimo (experimental)
* Liveview
* HPlus Devices (e.g. ZeBand)
## Features (Pebble)
@ -107,26 +108,33 @@ For more information read [this wiki article](https://github.com/Freeyourgadget/
3. Tap the Mi Band item to connect if you're not connected yet
4. To test, chose "Debug" from the menu and play around
Known Issues:
**Known Issues:**
* The initial connection to a Mi Band sometimes takes a little patience. Try to connect a few times, wait,
and try connecting again. This only happens until you have "bonded" with the Mi Band, i.e. until it
knows your MAC address. This behavior may also only occur with older firmware versions.
* If you use other apps like Mi Fit, and "bonding" with Gadgetbridge does not work, please
try to unpair the band in the other app and try again with Gadgetbridge.
* While all Mi Band devices are supported, some firmware versions might work better than others.
You can consult the [projects wiki pages](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
to check if your firmware version is fully supported or if an upgrade/downgrade might be beneficial.
## Features (Liveview)
* set time (automatically upon connection)
* display notifications and vibrate
## Authors (in order of first code contribution)
## Authors
### Core Team (in order of first code contribution)
* Andreas Shimokawa
* Carsten Pfeiffer
* Daniele Gobbetti
### Additional device support
* João Paulo Barraca (HPlus)
## Contribute
Contributions are welcome, be it feedback, bugreports, documentation, translation, research or code. Feel free to work

View File

@ -26,9 +26,8 @@ android {
targetSdkVersion 23
// note: always bump BOTH versionCode and versionName!
versionName "0.15.0"
versionCode 77
versionName "0.17.5"
versionCode 86
vectorDrawables.useSupportLibrary = true
}
buildTypes {

View File

@ -15,9 +15,9 @@
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.fsck.k9.permission.READ_MESSAGES" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-feature
android:name="android.hardware.bluetooth"
@ -191,6 +191,15 @@
<data android:mimeType="application/zip" />
<data android:mimeType="application/x-zip-compressed" />
</intent-filter>
<!-- to receive files from the "share" intent -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<service
@ -203,6 +212,28 @@
</service>
<service android:name=".service.DeviceCommunicationService" />
<receiver
android:name=".externalevents.WeatherNotificationReceiver"
android:enabled="true">
<intent-filter>
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_UPDATE_2" />
</intent-filter>
</receiver>
<activity android:name=".externalevents.WeatherNotificationConfig">
<intent-filter>
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_SKIN_PREFERENCES"/>
</intent-filter>
</activity>
<receiver android:name=".externalevents.AutoStartReceiver"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.BluetoothStateChangeReceiver"
android:exported="false">
@ -241,11 +272,6 @@
android:parentActivityName=".activities.ControlCenter"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.OnboardingActivity"
android:label="@string/title_activity_onboarding"
android:parentActivityName=".activities.ControlCenter"
android:screenOrientation="portrait" />
<activity
android:name=".activities.DiscoveryActivity"
android:label="@string/title_activity_discovery"

View File

@ -1,3 +1,9 @@
navigator.geolocation.getCurrentPosition = function(success, failure) { //override because default implementation requires GPS permission
success(JSON.parse(GBjs.getCurrentPosition()));
failure({ code: 2, message: "POSITION_UNAVAILABLE"});
}
if (window.Storage){
var prefix = GBjs.getAppLocalstoragePrefix();
GBjs.gbLog("redefining local storage with prefix: " + prefix);

View File

@ -27,7 +27,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import nodomain.freeyourgadget.gadgetbridge.database.DBConstants;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
@ -171,6 +170,10 @@ public class GBApplication extends Application {
return prefs.getBoolean("log_to_file", false);
}
public static boolean minimizeNotification() {
return prefs.getBoolean("minimize_priority", false);
}
static void setupDatabase(Context context) {
DBOpenHelper helper = new DBOpenHelper(context, DATABASE_NAME, null);
SQLiteDatabase db = helper.getWritableDatabase();
@ -331,11 +334,7 @@ public class GBApplication extends Application {
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);
}
boolean result = deleteOldActivityDatabase(context);
result &= getContext().deleteDatabase(DATABASE_NAME);
return result;
}
@ -348,8 +347,8 @@ public class GBApplication extends Application {
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);
if (dbHelper.existsDB("ActivityDatabase")) {
result = getContext().deleteDatabase("ActivityDatabase");
}
return result;
}

View File

@ -1,15 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.format.DateFormat;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.TimePicker;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
public class AlarmDetails extends GBActivity {
@ -23,16 +27,19 @@ public class AlarmDetails extends GBActivity {
private CheckBox cbFriday;
private CheckBox cbSaturday;
private CheckBox cbSunday;
private GBDevice device;
private TextView smartAlarmLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alarm_details);
Parcelable p = getIntent().getExtras().getParcelable("alarm");
alarm = (GBAlarm) p;
alarm = getIntent().getParcelableExtra("alarm");
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
timePicker = (TimePicker) findViewById(R.id.alarm_time_picker);
smartAlarmLabel = (TextView) findViewById(R.id.alarm_label_smart_wakeup);
cbSmartWakeup = (CheckBox) findViewById(R.id.alarm_cb_smart_wakeup);
cbMonday = (CheckBox) findViewById(R.id.alarm_cb_mon);
cbTuesday = (CheckBox) findViewById(R.id.alarm_cb_tue);
@ -47,6 +54,9 @@ public class AlarmDetails extends GBActivity {
timePicker.setCurrentMinute(alarm.getMinute());
cbSmartWakeup.setChecked(alarm.isSmartWakeup());
int smartAlarmVisibility = supportsSmartWakeup() ? View.VISIBLE : View.GONE;
cbSmartWakeup.setVisibility(smartAlarmVisibility);
smartAlarmLabel.setVisibility(smartAlarmVisibility);
cbMonday.setChecked(alarm.getRepetition(GBAlarm.ALARM_MON));
cbTuesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_TUE));
@ -58,6 +68,14 @@ public class AlarmDetails extends GBActivity {
}
private boolean supportsSmartWakeup() {
if (device != null) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator.supportsSmartWakeup(device);
}
return false;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {

View File

@ -14,6 +14,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBAlarmListAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ALARMS;
@ -26,6 +27,7 @@ public class ConfigureAlarms extends GBActivity {
private GBAlarmListAdapter mGBAlarmListAdapter;
private Set<String> preferencesAlarmListSet;
private boolean avoidSendAlarmsToDevice;
private GBDevice device;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -33,6 +35,8 @@ public class ConfigureAlarms extends GBActivity {
setContentView(R.layout.activity_configure_alarms);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
Prefs prefs = GBApplication.getPrefs();
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
if (preferencesAlarmListSet.isEmpty()) {
@ -86,12 +90,16 @@ public class ConfigureAlarms extends GBActivity {
public void configureAlarm(GBAlarm alarm) {
avoidSendAlarmsToDevice = true;
Intent startIntent;
startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
Intent startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
startIntent.putExtra("alarm", alarm);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
startActivityForResult(startIntent, REQ_CONFIGURE_ALARM);
}
private GBDevice getDevice() {
return device;
}
private void sendAlarmsToDevice() {
GBApplication.deviceService().onSetAlarms(mGBAlarmListAdapter.getAlarmList());
}

View File

@ -278,6 +278,7 @@ public class ControlCenter extends GBActivity {
if (selectedDevice != null) {
Intent startIntent;
startIntent = new Intent(ControlCenter.this, ConfigureAlarms.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, selectedDevice);
startActivity(startIntent);
}
return true;
@ -424,8 +425,6 @@ public class ControlCenter extends GBActivity {
wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CALENDAR);
if (ContextCompat.checkSelfPermission(this, "com.fsck.k9.permission.READ_MESSAGES") == PackageManager.PERMISSION_DENIED)
wantedPermissions.add("com.fsck.k9.permission.READ_MESSAGES");
if (!wantedPermissions.isEmpty())
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0);

View File

@ -1,11 +1,9 @@
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;
@ -18,16 +16,11 @@ 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;
@ -37,7 +30,6 @@ public class DbManagementActivity extends GBActivity {
private Button exportDBButton;
private Button importDBButton;
private Button importOldActivityDataButton;
private Button deleteOldActivityDBButton;
private Button deleteDBButton;
private TextView dbPath;
@ -68,22 +60,7 @@ public class DbManagementActivity extends GBActivity {
}
});
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();
}
});
int oldDBVisibility = hasOldActivityDatabase() ? View.VISIBLE : View.GONE;
deleteOldActivityDBButton = (Button) findViewById(R.id.deleteOldActivityDB);
deleteOldActivityDBButton.setVisibility(oldDBVisibility);
@ -104,7 +81,7 @@ public class DbManagementActivity extends GBActivity {
}
private boolean hasOldActivityDatabase() {
return new DBHelper(this).getOldActivityDatabaseHandler() != null;
return new DBHelper(this).existsDB("ActivityDatabase");
}
private String getExternalPath() {
@ -156,67 +133,6 @@ public class DbManagementActivity extends GBActivity {
.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<Object, ProgressDialog, Object>() {
@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<GBDevice> 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)
@ -271,8 +187,4 @@ public class DbManagementActivity extends GBActivity {
}
return super.onOptionsItemSelected(item);
}
public interface DeviceSelectionCallback {
void invoke(GBDevice device);
}
}

View File

@ -180,9 +180,10 @@ public class DebugActivity extends GBActivity {
@Override
public void onClick(View v) {
MusicSpec musicSpec = new MusicSpec();
musicSpec.artist = editContent.getText().toString() + "(artist)";
musicSpec.album = editContent.getText().toString() + "(album)";
musicSpec.track = editContent.getText().toString() + "(track)";
String testString = editContent.getText().toString();
musicSpec.artist = testString + "(artist)";
musicSpec.album = testString + "(album)";
musicSpec.track = testString + "(track)";
musicSpec.duration = 10;
musicSpec.trackCount = 5;
musicSpec.trackNr = 2;

View File

@ -293,6 +293,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
if (deviceType.isSupported()) {
candidate.setDeviceType(deviceType);
LOG.info("Recognized supported device: " + candidate);
int index = deviceCandidates.indexOf(candidate);
if (index >= 0) {
deviceCandidates.set(index, candidate); // replace
@ -506,10 +507,11 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
stopDiscovery();
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
LOG.info("Using device candidate " + deviceCandidate + " with coordinator: " + coordinator.getClass());
Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
if (pairingActivity != null) {
Intent intent = new Intent(this, pairingActivity);
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS, deviceCandidate.getMacAddress());
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
startActivity(intent);
} else {
try {

View File

@ -1,9 +1,15 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
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.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NavUtils;
import android.util.Log;
import android.view.MenuItem;
@ -39,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class ExternalPebbleJSActivity extends GBActivity {
@ -122,6 +129,10 @@ public class ExternalPebbleJSActivity extends GBActivity {
return null;
}
private boolean isLocationEnabledForWatchApp() {
return true; //as long as we don't give watchapp internet access it's not a problem
}
private class GBChromeClient extends WebChromeClient {
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
@ -306,6 +317,51 @@ public class ExternalPebbleJSActivity extends GBActivity {
public void closeActivity() {
NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext);
}
@JavascriptInterface
public String getCurrentPosition() {
if (!isLocationEnabledForWatchApp()) {
return "";
}
//we need to override this because the coarse location is not enough for the android webview, we should add the permission for fine location.
JSONObject geoPosition = new JSONObject();
JSONObject coords = new JSONObject();
try {
Prefs prefs = GBApplication.getPrefs();
geoPosition.put("timestamp", (System.currentTimeMillis() / 1000) - 86400); //let JS know this value is really old
coords.put("latitude", prefs.getFloat("location_latitude", 0));
coords.put("longitude", prefs.getFloat("location_longitude", 0));
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
prefs.getBoolean("use_updated_location_if_available", false)) {
LocationManager locationManager = (LocationManager) getApplicationContext().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) {
geoPosition.put("timestamp", lastKnownLocation.getTime());
coords.put("latitude", (float) lastKnownLocation.getLatitude());
coords.put("longitude", (float) lastKnownLocation.getLongitude());
coords.put("accuracy", lastKnownLocation.getAccuracy());
coords.put("altitude", lastKnownLocation.getAltitude());
coords.put("speed", lastKnownLocation.getSpeed());
}
}
}
geoPosition.put("coords", coords);
} catch (JSONException e) {
e.printStackTrace();
}
return geoPosition.toString();
}
}
@Override

View File

@ -152,6 +152,9 @@ public class FwAppInstallerActivity extends GBActivity implements InstallActivit
});
uri = getIntent().getData();
if (uri == null) { //for "share" intent
uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
}
installHandler = findInstallHandlerFor(uri);
if (installHandler == null) {
setInfoText(getString(R.string.installer_activity_unable_to_find_handler));

View File

@ -1,78 +0,0 @@
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<Object, ProgressDialog, Object>() {
@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);
}
}
}

View File

@ -4,6 +4,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@ -199,6 +200,9 @@ public abstract class AbstractAppManagerFragment extends Fragment {
if (baseName.equals("3af858c3-16cb-4561-91e7-f1ad2df8725f")) {
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
}
if (baseName.equals(PebbleProtocol.UUID_WEATHER.toString())) {
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WEATHER, "Weather (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
}
}
if (uuids == null) {
@ -292,12 +296,29 @@ public abstract class AbstractAppManagerFragment extends Fragment {
menu.removeItem(R.id.appmanager_hrm_activate);
menu.removeItem(R.id.appmanager_hrm_deactivate);
}
if (!PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_weather_activate);
menu.removeItem(R.id.appmanager_weather_deactivate);
menu.removeItem(R.id.appmanager_weather_install_provider);
}
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);
}
if (PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
PackageManager pm = getActivity().getPackageManager();
try {
pm.getPackageInfo("ru.gelin.android.weather.notification", PackageManager.GET_ACTIVITIES);
menu.removeItem(R.id.appmanager_weather_install_provider);
} catch (PackageManager.NameNotFoundException e) {
menu.removeItem(R.id.appmanager_weather_activate);
menu.removeItem(R.id.appmanager_weather_deactivate);
}
}
switch (selectedApp.getType()) {
case WATCHFACE:
case APP_GENERIC:
@ -367,10 +388,17 @@ public abstract class AbstractAppManagerFragment extends Fragment {
case R.id.appmanager_hrm_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://hrm"));
return true;
case R.id.appmanager_weather_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://weather"));
return true;
case R.id.appmanager_health_deactivate:
case R.id.appmanager_hrm_deactivate:
case R.id.appmanager_weather_deactivate:
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
return true;
case R.id.appmanager_weather_install_provider:
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/app/ru.gelin.android.weather.notification")));
return true;
case R.id.appmanager_app_configure:
GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);

View File

@ -1,16 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
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 android.view.View;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,6 +31,7 @@ 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.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -35,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class AppManagerActivity extends AbstractGBFragmentActivity {
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
private int READ_REQUEST_CODE = 42;
private GBDevice mGBDevice = null;
@ -68,6 +73,18 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
assert fab != null;
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
});
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
@ -93,7 +110,7 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@ -179,6 +196,16 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
return uuids;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Intent startIntent = new Intent(AppManagerActivity.this, FwAppInstallerActivity.class);
startIntent.setAction(Intent.ACTION_VIEW);
startIntent.setDataAndType(resultData.getData(), null);
startActivity(startIntent);
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);

View File

@ -16,7 +16,7 @@ public class AppManagerFragmentInstalledApps extends AbstractAppManagerFragment
//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(PebbleProtocol.UUID_NOTIFICATIONS, "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));
@ -28,6 +28,9 @@ public class AppManagerFragmentInstalledApps extends AbstractAppManagerFragment
if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) {
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WEATHER, "Weather (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
}
return systemApps;

View File

@ -0,0 +1,294 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
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.PieChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
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.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IValueFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
private Locale mLocale;
private int mTargetValue = 0;
private PieChart mTodayPieChart;
private BarChart mWeekChart;
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
Calendar day = Calendar.getInstance();
day.setTime(chartsHost.getEndDate());
//NB: we could have omitted the day, but this way we can move things to the past easily
DayData dayData = refreshDayPie(db, day, device);
DefaultChartsData weekBeforeData = refreshWeekBeforeData(db, mWeekChart, day, device);
return new MyChartsData(dayData, weekBeforeData);
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
// setupLegend(mWeekChart);
mTodayPieChart.setCenterText(mcd.getDayData().centerText);
mTodayPieChart.setData(mcd.getDayData().data);
mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mWeekChart.setData(mcd.getWeekBeforeData().getData());
mWeekChart.getLegend().setEnabled(false);
mWeekChart.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter());
}
@Override
protected void renderCharts() {
mWeekChart.invalidate();
mTodayPieChart.invalidate();
}
private DefaultChartsData<BarData> refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -7);
List<BarEntry> entries = new ArrayList<>();
ArrayList<String> labels = new ArrayList<String>();
for (int counter = 0; counter < 7; counter++) {
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
entries.add(new BarEntry(counter, getTotalForActivityAmounts(amounts)));
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
day.add(Calendar.DATE, 1);
}
BarDataSet set = new BarDataSet(entries, "");
set.setColor(getMainColor());
set.setValueFormatter(getFormatter());
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(mTargetValue);
barChart.getAxisLeft().removeAllLimitLines();
barChart.getAxisLeft().addLimitLine(target);
return new DefaultChartsData(barData, new PreformattedXIndexLabelFormatter(labels));
}
private DayData refreshDayPie(DBHandler db, Calendar day, GBDevice device) {
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
int totalValue = getTotalForActivityAmounts(amounts);
PieData data = new PieData();
List<PieEntry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
entries.add(new PieEntry(totalValue, "")); //we don't want labels on the pie chart
colors.add(getMainColor());
if (totalValue < mTargetValue) {
entries.add(new PieEntry((mTargetValue - totalValue))); //we don't want labels on the pie chart
colors.add(Color.GRAY);
}
PieDataSet set = new PieDataSet(entries, "");
set.setValueFormatter(getFormatter());
set.setColors(colors);
data.setDataSet(set);
//this hides the values (numeric) added to the set. These would be shown aside the strings set with addXValue above
data.setDrawValues(false);
return new DayData(data, formatPieValue(totalValue));
}
protected abstract String formatPieValue(int value);
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
int goal = getGoal();
if (goal >= 0) {
mTargetValue = goal;
}
mTodayPieChart = (PieChart) rootView.findViewById(R.id.todaystepschart);
mWeekChart = (BarChart) rootView.findViewById(R.id.weekstepschart);
setupWeekChart();
setupTodayPieChart();
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
return rootView;
}
private void setupTodayPieChart() {
mTodayPieChart.setBackgroundColor(BACKGROUND_COLOR);
mTodayPieChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mTodayPieChart.getDescription().setText(getContext().getString(R.string.weeksteps_today_steps_description, String.valueOf(mTargetValue)));
// mTodayPieChart.setNoDataTextDescription("");
mTodayPieChart.setNoDataText("");
mTodayPieChart.getLegend().setEnabled(false);
// setupLegend(mTodayPieChart);
}
private void setupWeekChart() {
mWeekChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mWeekChart.getDescription().setText("");
mWeekChart.setFitBars(true);
configureBarLineChartDefaults(mWeekChart);
XAxis x = mWeekChart.getXAxis();
x.setDrawLabels(true);
x.setDrawGridLines(false);
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
x.setPosition(XAxis.XAxisPosition.BOTTOM);
YAxis y = mWeekChart.getAxisLeft();
y.setDrawGridLines(false);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setDrawZeroLine(true);
y.setSpaceBottom(0);
y.setAxisMinimum(0);
y.setEnabled(true);
YAxis yAxisRight = mWeekChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
}
@Override
protected void setupLegend(Chart chart) {
// List<Integer> legendColors = new ArrayList<>(1);
// List<String> legendLabels = new ArrayList<>(1);
// legendColors.add(akActivity.color);
// legendLabels.add(getContext().getString(R.string.chart_steps));
// chart.getLegend().setCustom(legendColors, legendLabels);
// chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
int startTs;
int endTs;
day = (Calendar) day.clone(); // do not modify the caller's argument
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
startTs = (int) (day.getTimeInMillis() / 1000);
day.set(Calendar.HOUR_OF_DAY, 23);
day.set(Calendar.MINUTE, 59);
day.set(Calendar.SECOND, 59);
endTs = (int) (day.getTimeInMillis() / 1000);
return getSamples(db, device, startTs, endTs);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
private static class DayData {
private final PieData data;
private final CharSequence centerText;
DayData(PieData data, String centerText) {
this.data = data;
this.centerText = centerText;
}
}
private static class MyChartsData extends ChartsData {
private final DefaultChartsData<BarData> weekBeforeData;
private final DayData dayData;
MyChartsData(DayData dayData, DefaultChartsData<BarData> weekBeforeData) {
this.dayData = dayData;
this.weekBeforeData = weekBeforeData;
}
DayData getDayData() {
return dayData;
}
DefaultChartsData<BarData> getWeekBeforeData() {
return weekBeforeData;
}
}
private ActivityAmounts getActivityAmountsForDay(DBHandler db, Calendar day, GBDevice device) {
LimitedQueue activityAmountCache = null;
ActivityAmounts amounts = null;
Activity activity = getActivity();
if (activity != null) {
activityAmountCache = ((ChartsActivity) activity).mActivityAmountCache;
amounts = (ActivityAmounts) (activityAmountCache.lookup(day.hashCode()));
}
if (amounts == null) {
ActivityAnalysis analysis = new ActivityAnalysis();
amounts = analysis.calculateActivityAmounts(getSamplesOfDay(db, day, device));
if (activityAmountCache != null) {
activityAmountCache.add(day.hashCode(), amounts);
}
}
return amounts;
}
abstract int getGoal();
abstract int getTotalForActivityAmounts(ActivityAmounts activityAmounts);
abstract IValueFormatter getFormatter();
abstract Integer getMainColor();
}

View File

@ -7,8 +7,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class ActivityAnalysis {
public ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
class ActivityAnalysis {
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);
@ -34,6 +34,11 @@ public class ActivityAnalysis {
break;
}
int steps = sample.getSteps();
if (steps > 0) {
amount.addSteps(sample.getSteps());
}
if (previousSample != null) {
long timeDifference = sample.getTimestamp() - previousSample.getTimestamp();
if (previousSample.getRawKind() == sample.getRawKind()) {
@ -64,7 +69,7 @@ public class ActivityAnalysis {
return result;
}
public int calculateTotalSteps(List<? extends ActivitySample> samples) {
int calculateTotalSteps(List<? extends ActivitySample> samples) {
int totalSteps = 0;
for (ActivitySample sample : samples) {
int steps = sample.getSteps();

View File

@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public class ChartsActivity extends AbstractGBFragmentActivity implements ChartsHost {
@ -52,11 +53,13 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
private PagerTabStrip mPagerTabStrip;
private ViewPager viewPager;
LimitedQueue mActivityAmountCache = new LimitedQueue(32);
private static class ShowDurationDialog extends Dialog {
private final String mDuration;
private TextView durationLabel;
public ShowDurationDialog(String duration, Context context) {
ShowDurationDialog(String duration, Context context) {
super(context);
mDuration = duration;
}
@ -298,7 +301,7 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
*/
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@ -311,8 +314,10 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
case 1:
return new SleepChartFragment();
case 2:
return new WeekStepsChartFragment();
return new WeekSleepChartFragment();
case 3:
return new WeekStepsChartFragment();
case 4:
return new LiveActivityFragment();
}
@ -321,8 +326,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
@Override
public int getCount() {
// Show 3 total pages.
return 4;
// Show 5 total pages.
return 5;
}
@Override
@ -333,8 +338,10 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
case 1:
return getString(R.string.sleepchart_your_sleep);
case 2:
return getString(R.string.weekstepschart_steps_a_week);
return getString(R.string.weeksleepchart_sleep_a_week);
case 3:
return getString(R.string.weekstepschart_steps_a_week);
case 4:
return getString(R.string.liveactivity_live_activity);
}
return super.getPageTitle(position);

View File

@ -72,8 +72,6 @@ public class LiveActivityFragment extends AbstractChartFragment {
private TimestampTranslation tsTranslation;
private class Steps {
private int initialSteps;
private int steps;
private int lastTimestamp;
private int currentStepsPerMinute;
@ -90,39 +88,29 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
public int getTotalSteps() {
return steps - initialSteps;
return steps;
}
public int getMaxStepsPerMinute() {
return maxStepsPerMinute;
}
public void updateCurrentSteps(int newSteps, int timestamp) {
public void updateCurrentSteps(int stepsDelta, int timestamp) {
try {
if (steps == 0) {
steps = newSteps;
steps += stepsDelta;
lastTimestamp = timestamp;
if (newSteps > 0) {
initialSteps = newSteps;
}
return;
}
if (newSteps >= steps) {
int stepsDelta = newSteps - steps;
int timeDelta = timestamp - lastTimestamp;
currentStepsPerMinute = calculateStepsPerMinute(stepsDelta, timeDelta);
if (currentStepsPerMinute > maxStepsPerMinute) {
maxStepsPerMinute = currentStepsPerMinute;
maxStepsResetCounter = 0;
}
steps = newSteps;
lastTimestamp = timestamp;
} else {
// TODO: handle new day?
int timeDelta = timestamp - lastTimestamp;
currentStepsPerMinute = calculateStepsPerMinute(stepsDelta, timeDelta);
if (currentStepsPerMinute > maxStepsPerMinute) {
maxStepsPerMinute = currentStepsPerMinute;
maxStepsResetCounter = 0;
}
steps += stepsDelta;
lastTimestamp = timestamp;
} catch (Exception ex) {
GB.toast(LiveActivityFragment.this.getContext(), ex.getMessage(), Toast.LENGTH_SHORT, GB.ERROR, ex);
}
@ -136,7 +124,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
throw new IllegalArgumentException("delta in seconds is <= 0 -- time change?");
}
int oneMinute = 60 * 1000;
int oneMinute = 60;
float factor = oneMinute / seconds;
int result = (int) (stepsDelta * factor);
if (result > MAX_STEPS_PER_MINUTE) {
@ -152,24 +140,27 @@ public class LiveActivityFragment extends AbstractChartFragment {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case DeviceService.ACTION_REALTIME_STEPS: {
int steps = intent.getIntExtra(DeviceService.EXTRA_REALTIME_STEPS, 0);
int timestamp = translateTimestampFrom(intent);
addEntries(steps, timestamp);
break;
}
case DeviceService.ACTION_HEARTRATE_MEASUREMENT: {
int heartRate = intent.getIntExtra(DeviceService.EXTRA_HEART_RATE_VALUE, 0);
int timestamp = translateTimestampFrom(intent);
if (isValidHeartRateValue(heartRate)) {
setCurrentHeartRate(heartRate, timestamp);
}
case DeviceService.ACTION_REALTIME_SAMPLES: {
ActivitySample sample = (ActivitySample) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE);
addSample(sample);
break;
}
}
}
};
private void addSample(ActivitySample sample) {
int heartRate = sample.getHeartRate();
int timestamp = tsTranslation.shorten(sample.getTimestamp());
if (isValidHeartRateValue(heartRate)) {
setCurrentHeartRate(heartRate, timestamp);
}
int steps = sample.getSteps();
if (steps != ActivitySample.NOT_MEASURED) {
addEntries(steps, timestamp);
}
}
private int translateTimestampFrom(Intent intent) {
return translateTimestamp(intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()));
}
@ -251,8 +242,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(DeviceService.ACTION_REALTIME_STEPS);
filterLocal.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
heartRateValues = new ArrayList<>();
tsTranslation = new TimestampTranslation();
@ -377,6 +367,9 @@ public class LiveActivityFragment extends AbstractChartFragment {
// chart.getXAxis().setPosition(XAxis.XAxisPosition.TOP);
chart.getXAxis().setDrawLabels(false);
chart.getXAxis().setEnabled(false);
chart.getXAxis().setTextColor(CHART_TEXT_COLOR);
chart.getAxisLeft().setTextColor(CHART_TEXT_COLOR);
chart.setBackgroundColor(BACKGROUND_COLOR);
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
chart.getDescription().setText(title);

View File

@ -0,0 +1,56 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class WeekSleepChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weeksleepchart_sleep_a_week);
}
@Override
int getGoal() {
return 8 * 60; // FIXME
}
@Override
int getTotalForActivityAmounts(ActivityAmounts activityAmounts) {
long totalSeconds = 0;
for (ActivityAmount amount : activityAmounts.getAmounts()) {
if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) {
totalSeconds += amount.getTotalSeconds();
}
}
return (int) (totalSeconds / 60);
}
@Override
protected String formatPieValue(int value) {
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.MINUTES);
}
@Override
IValueFormatter getFormatter() {
return new IValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
return formatPieValue((int) value);
}
};
}
@Override
Integer getMainColor() {
return akLightSleep.color;
}
}

View File

@ -1,267 +1,50 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.graphics.Color;
import android.os.Bundle;
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.PieChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
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.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import com.github.mikephil.charting.formatter.IValueFormatter;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class WeekStepsChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(WeekStepsChartFragment.class);
private Locale mLocale;
private int mTargetSteps = 10000;
private PieChart mTodayStepsChart;
private BarChart mWeekStepsChart;
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
Calendar day = Calendar.getInstance();
day.setTime(chartsHost.getEndDate());
//NB: we could have omitted the day, but this way we can move things to the past easily
DaySteps daySteps = refreshDaySteps(db, day, device);
DefaultChartsData weekBeforeStepsData = refreshWeekBeforeSteps(db, mWeekStepsChart, day, device);
return new MyChartsData(daySteps, weekBeforeStepsData);
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
// setupLegend(mWeekStepsChart);
mTodayStepsChart.setCenterText(NumberFormat.getNumberInstance(mLocale).format(mcd.getDaySteps().totalSteps));
mTodayStepsChart.setData(mcd.getDaySteps().data);
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
protected void renderCharts() {
mWeekStepsChart.invalidate();
mTodayStepsChart.invalidate();
}
private DefaultChartsData<BarData> 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<BarEntry> entries = new ArrayList<>();
ArrayList<String> labels = new ArrayList<String>();
for (int counter = 0; counter < 7; 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);
}
BarDataSet set = new BarDataSet(entries, "");
set.setColor(akActivity.color);
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);
barChart.getAxisLeft().removeAllLimitLines();
barChart.getAxisLeft().addLimitLine(target);
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<PieEntry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
entries.add(new PieEntry(totalSteps, "")); //we don't want labels on the pie chart
colors.add(akActivity.color);
if (totalSteps < mTargetSteps) {
entries.add(new PieEntry((mTargetSteps - totalSteps))); //we don't want labels on the pie chart
colors.add(Color.GRAY);
}
PieDataSet set = new PieDataSet(entries, "");
set.setColors(colors);
data.setDataSet(set);
//this hides the values (numeric) added to the set. These would be shown aside the strings set with addXValue above
data.setDrawValues(false);
return new DaySteps(data, totalSteps);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
GBDevice device = getChartsHost().getDevice();
if (device != null) {
// TODO: eek, this is device specific!
mTargetSteps = MiBandCoordinator.getFitnessGoal(device.getAddress());
}
mTodayStepsChart = (PieChart) rootView.findViewById(R.id.todaystepschart);
mWeekStepsChart = (BarChart) rootView.findViewById(R.id.weekstepschart);
setupWeekStepsChart();
setupTodayStepsChart();
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
return rootView;
}
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
public class WeekStepsChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weekstepschart_steps_a_week);
}
private void setupTodayStepsChart() {
mTodayStepsChart.setBackgroundColor(BACKGROUND_COLOR);
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);
}
private void setupWeekStepsChart() {
mWeekStepsChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekStepsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mWeekStepsChart.getDescription().setText("");
mWeekStepsChart.setFitBars(true);
configureBarLineChartDefaults(mWeekStepsChart);
XAxis x = mWeekStepsChart.getXAxis();
x.setDrawLabels(true);
x.setDrawGridLines(false);
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);
YAxis yAxisRight = mWeekStepsChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
@Override
int getGoal() {
GBDevice device = getChartsHost().getDevice();
if (device != null) {
return MiBandCoordinator.getFitnessGoal(device.getAddress());
}
return -1;
}
@Override
protected void setupLegend(Chart chart) {
// List<Integer> legendColors = new ArrayList<>(1);
// List<String> legendLabels = new ArrayList<>(1);
// legendColors.add(akActivity.color);
// legendLabels.add(getContext().getString(R.string.chart_steps));
// chart.getLegend().setCustom(legendColors, legendLabels);
// chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
int startTs;
int endTs;
day = (Calendar) day.clone(); // do not modify the caller's argument
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
startTs = (int) (day.getTimeInMillis() / 1000);
day.set(Calendar.HOUR_OF_DAY, 23);
day.set(Calendar.MINUTE, 59);
day.set(Calendar.SECOND, 59);
endTs = (int) (day.getTimeInMillis() / 1000);
return getSamples(db, device, startTs, endTs);
int getTotalForActivityAmounts(ActivityAmounts activityAmounts) {
int totalSteps = 0;
for (ActivityAmount amount : activityAmounts.getAmounts()) {
totalSteps += amount.getTotalSteps();
amount.getTotalSteps();
}
return totalSteps;
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
protected String formatPieValue(int value) {
return String.valueOf(value);
}
private static class DaySteps {
private final PieData data;
private final int totalSteps;
public DaySteps(PieData data, int totalSteps) {
this.data = data;
this.totalSteps = totalSteps;
}
@Override
IValueFormatter getFormatter() {
return null;
}
private static class MyChartsData extends ChartsData {
private final DefaultChartsData<BarData> weekBeforeStepsData;
private final DaySteps daySteps;
public MyChartsData(DaySteps daySteps, DefaultChartsData<BarData> weekBeforeStepsData) {
this.daySteps = daySteps;
this.weekBeforeStepsData = weekBeforeStepsData;
}
public DaySteps getDaySteps() {
return daySteps;
}
public DefaultChartsData<BarData> getWeekBeforeStepsData() {
return weekBeforeStepsData;
}
@Override
Integer getMainColor() {
return akActivity.color;
}
}

View File

@ -134,6 +134,14 @@ public class GBDeviceAdapter extends ArrayAdapter<GBDevice> {
deviceImageView.setImageResource(R.drawable.ic_device_lovetoy_disabled);
}
break;
case HPLUS:
case MAKIBESF68:
if( device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_device_hplus);
} else {
deviceImageView.setImageResource(R.drawable.ic_device_hplus_disabled);
}
break;
default:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_launcher);

View File

@ -58,15 +58,17 @@ public class PebbleContentProvider extends ContentProvider {
if (uri.equals(CONTENT_URI)) {
MatrixCursor mc = new MatrixCursor(columnNames);
int connected = 0;
int appMessage = 0;
int pebbleKit = 0;
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("pebble_enable_pebblekit", false)) {
appMessage = 1;
pebbleKit = 1;
}
String fwString = "unknown";
if (mGBDevice != null && mGBDevice.getType() == DeviceType.PEBBLE && mGBDevice.isInitialized()) {
connected = 1;
fwString = mGBDevice.getFirmwareVersion();
}
mc.addRow(new Object[]{connected, appMessage, 0, 3, 8, 2, "Gadgetbridge"});
mc.addRow(new Object[]{connected, pebbleKit, pebbleKit, 3, 8, 2, fwString});
return mc;
} else {

View File

@ -1,105 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import java.io.File;
import nodomain.freeyourgadget.gadgetbridge.database.schema.ActivityDBCreationScript;
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_TIMESTAMP;
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 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
public void onCreate(SQLiteDatabase db) {
try {
ActivityDBCreationScript script = new ActivityDBCreationScript();
script.createSchema(db);
} catch (RuntimeException ex) {
GB.toast("Error creating database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
new SchemaMigration(UPDATER_CLASS_NAME_PREFIX).onUpgrade(db, oldVersion, newVersion);
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
new SchemaMigration(UPDATER_CLASS_NAME_PREFIX).onDowngrade(db, oldVersion, newVersion);
}
@Override
public SQLiteDatabase getDatabase() {
return super.getWritableDatabase();
}
@Override
public void closeDb() {
}
@Override
public void openDb() {
}
@Override
public SQLiteOpenHelper getHelper() {
return this;
}
public Context getContext() {
return context;
}
public boolean hasContent() {
File dbFile = getContext().getDatabasePath(getDatabaseName());
if (dbFile == null || !dbFile.exists()) {
return false;
}
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();
}
}
} catch (Exception ex) {
// can't expect anything
GB.log("Error looking for old activity data: " + ex.getMessage(), GB.ERROR, ex);
return false;
}
}
@Override
public DaoSession getDaoSession() {
throw new UnsupportedOperationException();
}
@Override
public DaoMaster getDaoMaster() {
throw new UnsupportedOperationException();
}
}

View File

@ -1,18 +0,0 @@
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 KEY_TIMESTAMP = "timestamp";
public static final String KEY_PROVIDER = "provider";
public static final String KEY_INTENSITY = "intensity";
public static final String KEY_STEPS = "steps";
public static final String KEY_CUSTOM_SHORT = "customShort";
public static final String KEY_TYPE = "type";
}

View File

@ -6,7 +6,6 @@ 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;
@ -14,7 +13,6 @@ 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;
@ -26,11 +24,7 @@ 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;
@ -38,29 +32,18 @@ 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
@ -547,149 +530,6 @@ public class DBHelper {
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 <T extends AbstractActivitySample> void importActivitySamples(SQLiteDatabase fromDb, GBDevice targetDevice, DaoSession targetSession, AbstractSampleProvider<T> 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<PebbleHealthActivityOverlay> overlayList = new ArrayList<>();
final int BATCH_SIZE = 100000; // 100.000 samples = rougly 20 MB per batch
List<T> 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();

View File

@ -1,27 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
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;
public class ActivityDBCreationScript {
public void createSchema(SQLiteDatabase db) {
String CREATE_GBACTIVITYSAMPLES_TABLE = "CREATE TABLE " + TABLE_GBACTIVITYSAMPLES + " ("
+ KEY_TIMESTAMP + " INT,"
+ KEY_PROVIDER + " TINYINT,"
+ KEY_INTENSITY + " SMALLINT,"
+ KEY_STEPS + " TINYINT,"
+ KEY_TYPE + " TINYINT,"
+ KEY_CUSTOM_SHORT + " INT,"
+ " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId();
db.execSQL(CREATE_GBACTIVITYSAMPLES_TABLE);
}
}

View File

@ -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.TABLE_GBACTIVITYSAMPLES;
/**
* Upgrade and downgrade with DB versions <= 5 is not supported.
* Just recreates the default schema. Those GB versions may or may not
* work with that, but this code will probably not create a DB for them
* anyway.
*/
public class ActivityDBUpdate_4 extends ActivityDBCreationScript implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
recreateSchema(db);
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
recreateSchema(db);
}
private void recreateSchema(SQLiteDatabase db) {
DBHelper.dropTable(TABLE_GBACTIVITYSAMPLES, db);
createSchema(db);
}
}

View File

@ -1,27 +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_CUSTOM_SHORT;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
/**
* Adds a column "customShort" to the table "GBActivitySamples"
*/
public class ActivityDBUpdate_6 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(TABLE_GBACTIVITYSAMPLES, KEY_CUSTOM_SHORT, db)) {
String ADD_COLUMN_CUSTOM_SHORT = "ALTER TABLE " + TABLE_GBACTIVITYSAMPLES + " ADD COLUMN "
+ KEY_CUSTOM_SHORT + " INT;";
db.execSQL(ADD_COLUMN_CUSTOM_SHORT);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -1,8 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
/**
* Bugfix for users who installed 0.8.1 cleanly, i.e. without any previous
* database. Perform Update script 6 again.
*/
public class ActivityDBUpdate_7 extends ActivityDBUpdate_6 {
}

View File

@ -0,0 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.deviceevents.pebble;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
public class GBDeviceEventDataLogging extends GBDeviceEvent {
public static final int COMMAND_RECEIVE_DATA = 1;
public static final int COMMAND_FINISH_SESSION = 2;
public int command;
public UUID appUUID;
public long timestamp;
public long tag;
public byte pebbleDataType;
public Object[] data;
}

View File

@ -27,7 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
* the given device.
*/
public interface DeviceCoordinator {
String EXTRA_DEVICE_MAC_ADDRESS = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_MAC_ADDRESS";
String EXTRA_DEVICE_CANDIDATE = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_DEVICE_CANDIDATE";
/**
* Checks whether this coordinator handles the given candidate.
@ -154,6 +154,12 @@ public interface DeviceCoordinator {
*/
boolean supportsAlarmConfiguration();
/**
* Returns true if this device/coordinator supports alarms with smart wakeup
* @return
*/
boolean supportsSmartWakeup(GBDevice device);
/**
* Returns true if the given device supports heart rate measurements.
* @return

View File

@ -12,6 +12,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
/**
* Specifies all events that Gadgetbridge intends to send to the gadget device.
@ -21,6 +22,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
public interface EventHandler {
void onNotification(NotificationSpec notificationSpec);
void onDeleteNotification(int id);
void onSetTime();
void onSetAlarms(ArrayList<? extends Alarm> alarms);
@ -75,4 +78,6 @@ public interface EventHandler {
void onSendConfiguration(String config);
void onTestNewFunction();
void onSendWeather(WeatherSpec weatherSpec);
}

View File

@ -24,6 +24,7 @@ public interface SampleProvider<T extends AbstractActivitySample> {
int PROVIDER_PEBBLE_MISFIT = 3;
int PROVIDER_PEBBLE_HEALTH = 4;
int PROVIDER_MIBAND2 = 5;
int PROVIDER_HPLUS = 6;
int PROVIDER_UNKNOWN = 100;
// TODO: can also be removed

View File

@ -135,6 +135,11 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;

View File

@ -0,0 +1,173 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public final class HPlusConstants {
public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("14702856-620a-3973-7c78-9cfff0876abd");
public static final UUID UUID_CHARACTERISTIC_MEASURE = UUID.fromString("14702853-620a-3973-7c78-9cfff0876abd");
public static final UUID UUID_SERVICE_HP = UUID.fromString("14701820-620a-3973-7c78-9cfff0876abd");
public static final byte ARG_WRIST_LEFT = 0; //Guess...
public static final byte ARG_WRIST_RIGHT = 1; //Guess...
public static final byte ARG_LANGUAGE_CN = 1;
public static final byte ARG_LANGUAGE_EN = 2;
public static final byte ARG_TIMEMODE_24H = 1;
public static final byte ARG_TIMEMODE_12H = 0;
public static final byte ARG_UNIT_METRIC = 0;
public static final byte ARG_UNIT_IMPERIAL = 1;
public static final byte ARG_GENDER_MALE = 0;
public static final byte ARG_GENDER_FEMALE = 1;
public static final byte ARG_HEARTRATE_MEASURE_ON = 11;
public static final byte ARG_HEARTRATE_MEASURE_OFF = 22;
public static final byte ARG_HEARTRATE_ALLDAY_ON = 0x0A;
public static final byte ARG_HEARTRATE_ALLDAY_OFF = (byte) 0xff;
public static final byte INCOMING_CALL_STATE_DISABLED_THRESHOLD = 0x7B;
public static final byte INCOMING_CALL_STATE_ENABLED = (byte) 0xAA;
public static final byte ARG_ALARM_DISABLE = (byte) -1;
public static final byte[] CMD_SET_PREF_START = new byte[]{0x4f, 0x5a};
public static final byte[] CMD_SET_PREF_START1 = new byte[]{0x4d};
//public static final byte CMD_SET_ALARM = 0x4c; Unknown
public static final byte CMD_SET_ALARM = 0x0c;
public static final byte CMD_SET_LANGUAGE = 0x22;
public static final byte CMD_SET_TIMEMODE = 0x47;
public static final byte CMD_SET_UNITS = 0x48;
public static final byte CMD_SET_GENDER = 0x2d;
public static final byte CMD_SET_DATE = 0x08;
public static final byte CMD_SET_TIME = 0x09;
public static final byte CMD_SET_WEEK = 0x2a;
public static final byte CMD_SET_PREF_SIT = 0x1e;
public static final byte CMD_SET_WEIGHT = 0x05;
public static final byte CMD_HEIGHT = 0x04;
public static final byte CMD_SET_AGE = 0x2c;
public static final byte CMD_SET_GOAL = 0x26;
public static final byte CMD_SET_SCREENTIME = 0x0b;
public static final byte CMD_SET_BLOOD = 0x4e; //??
public static final byte CMD_SET_FINDME = 0x0a;
public static final byte ARG_FINDME_ON = 0x01;
public static final byte ARG_FINDME_OFF = 0x02;
public static final byte CMD_GET_VERSION = 0x17;
public static final byte CMD_SET_END = 0x4f;
public static final byte CMD_SET_INCOMING_CALL_NUMBER = 0x23;
public static final byte CMD_SET_ALLDAY_HRM = 0x35;
public static final byte CMD_ACTION_INCOMING_CALL = 0x41;
public static final byte CMD_SET_CONF_END = 0x4f;
public static final byte CMD_SET_PREFS = 0x50;
public static final byte CMD_SET_SIT_INTERVAL = 0x51;
public static final byte CMD_SET_HEARTRATE_STATE = 0x32;
//Actions to device
public static final byte CMD_GET_ACTIVE_DAY = 0x27;
public static final byte CMD_GET_DAY_DATA = 0x15;
public static final byte CMD_GET_SLEEP = 0x19;
public static final byte CMD_GET_CURR_DATA = 0x16;
public static final byte CMD_GET_DEVICE_ID = 0x24;
public static final byte CMD_ACTION_INCOMING_SOCIAL = 0x31;
//public static final byte COMMAND_ACTION_INCOMING_SMS = 0x40; //Unknown
public static final byte CMD_ACTION_DISPLAY_TEXT = 0x43;
public static final byte CMD_ACTION_DISPLAY_TEXT_NAME = 0x3F;
public static final byte CMD_ACTION_DISPLAY_TEXT_NAME_CN = 0x3E; //Text in GB2312?
public static final byte[] CMD_ACTION_HELLO = new byte[]{0x01, 0x00};
public static final byte CMD_SHUTDOWN = 0x5B;
public static final byte ARG_SHUTDOWN_EN = 0x5A;
public static final byte CMD_FACTORY_RESET = -74;
public static final byte ARG_FACTORY_RESET_EN = 0x5A;
public static final byte CMD_SET_INCOMING_MESSAGE = 0x07;
public static final byte CMD_SET_INCOMING_CALL = 0x06;
public static final byte ARG_INCOMING_CALL = (byte) -86;
public static final byte ARG_INCOMING_MESSAGE = (byte) -86;
//Incoming Messages
public static final byte DATA_STATS = 0x33;
public static final byte DATA_STEPS = 0x36;
public static final byte DATA_DAY_SUMMARY = 0x38;
public static final byte DATA_DAY_SUMMARY_ALT = 0x39;
public static final byte DATA_SLEEP = 0x1A;
public static final byte DATA_VERSION = 0x18;
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";
public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr";
public static final String PREF_HPLUS_UNIT = "hplus_unit";
public static final String PREF_HPLUS_TIMEFORMAT = "hplus_timeformat";
public static final String PREF_HPLUS_WRIST = "hplus_wrist";
public static final String PREF_HPLUS_SIT_START_TIME = "hplus_sit_start_time";
public static final String PREF_HPLUS_SIT_END_TIME = "hplus_sit_end_time";
public static final Map<Character, Byte> transliterateMap = new HashMap<Character, Byte>(){
{
//These are missing
put('ó', new Byte((byte) 111));
put('Ó', new Byte((byte) 79));
put('í', new Byte((byte) 105));
put('Í', new Byte((byte) 73));
put('ú', new Byte((byte) 117));
put('Ú', new Byte((byte) 85));
//These mostly belong to the extended ASCII table
put('Ç', new Byte((byte) 128));
put('ü', new Byte((byte) 129));
put('é', new Byte((byte) 130));
put('â', new Byte((byte) 131));
put('ä', new Byte((byte) 132));
put('à', new Byte((byte) 133));
put('ã', new Byte((byte) 134));
put('ç', new Byte((byte) 135));
put('ê', new Byte((byte) 136));
put('ë', new Byte((byte) 137));
put('è', new Byte((byte) 138));
put('Ï', new Byte((byte) 139));
put('Î', new Byte((byte) 140));
put('Ì', new Byte((byte) 141));
put('Ã', new Byte((byte) 142));
put('Ä', new Byte((byte) 143));
put('É', new Byte((byte) 144));
put('æ', new Byte((byte) 145));
put('Æ', new Byte((byte) 146));
put('ô', new Byte((byte) 147));
put('ö', new Byte((byte) 148));
put('ò', new Byte((byte) 149));
put('û', new Byte((byte) 150));
put('ù', new Byte((byte) 151));
put('ÿ', new Byte((byte) 152));
put('Ö', new Byte((byte) 153));
put('Ü', new Byte((byte) 154));
put('¢', new Byte((byte) 155));
put('£', new Byte((byte) 156));
put('¥', new Byte((byte) 157));
put('ƒ', new Byte((byte) 159));
put('á', new Byte((byte) 160));
put('ñ', new Byte((byte) 164));
put('Ñ', new Byte((byte) 165));
put('ª', new Byte((byte) 166));
put('º', new Byte((byte) 167));
put('¿', new Byte((byte) 168));
put('¬', new Byte((byte) 170));
put('½', new Byte((byte) 171));
put('¼', new Byte((byte) 172));
put('¡', new Byte((byte) 173));
put('«', new Byte((byte) 174));
put('»', new Byte((byte) 175));
}
};
}

View File

@ -0,0 +1,258 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
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.os.ParcelUuid;
import android.support.annotation.NonNull;
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.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
public class HPlusCoordinator extends AbstractDeviceCoordinator {
protected static final Logger LOG = LoggerFactory.getLogger(HPlusCoordinator.class);
protected static Prefs prefs = GBApplication.getPrefs();
@NonNull
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
ParcelUuid hpService = new ParcelUuid(HPlusConstants.UUID_SERVICE_HP);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(hpService).build();
return Collections.singletonList(filter);
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && name.startsWith("HPLUS")) {
return DeviceType.HPLUS;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HPLUS;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public Class<? extends Activity> getPrimaryActivity() {
return ChartsActivity.class;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new HPlusHealthSampleProvider(device, session);
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public boolean supportsAlarmConfiguration() {
return true;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Zeblaze";
}
@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 {
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getHPlusHealthActivitySampleDao().queryBuilder();
qb.where(HPlusHealthActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
public static byte getLanguage(String address) {
String language = prefs.getString("language", "default");
Locale locale;
if (language.equals("default")) {
locale = Locale.getDefault();
} else {
locale = new Locale(language);
}
if (locale.getLanguage().equals(new Locale("cn").getLanguage())){
return HPlusConstants.ARG_LANGUAGE_CN;
}else{
return HPlusConstants.ARG_LANGUAGE_EN;
}
}
public static byte getTimeMode(String address) {
String tmode = prefs.getString(HPlusConstants.PREF_HPLUS_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h));
if(tmode.equals(getContext().getString(R.string.p_timeformat_24h))) {
return HPlusConstants.ARG_TIMEMODE_24H;
}else{
return HPlusConstants.ARG_TIMEMODE_12H;
}
}
public static byte getUnit(String address) {
String units = prefs.getString(HPlusConstants.PREF_HPLUS_UNIT, getContext().getString(R.string.p_unit_metric));
if(units.equals(getContext().getString(R.string.p_unit_metric))){
return HPlusConstants.ARG_UNIT_METRIC;
}else{
return HPlusConstants.ARG_UNIT_IMPERIAL;
}
}
public static byte getUserWeight(String address) {
ActivityUser activityUser = new ActivityUser();
return (byte) (activityUser.getWeightKg() & 0xFF);
}
public static byte getUserHeight(String address) {
ActivityUser activityUser = new ActivityUser();
return (byte) (activityUser.getHeightCm() & 0xFF);
}
public static byte getUserAge(String address) {
ActivityUser activityUser = new ActivityUser();
return (byte) (activityUser.getAge() & 0xFF);
}
public static byte getUserGender(String address) {
ActivityUser activityUser = new ActivityUser();
if (activityUser.getGender() == ActivityUser.GENDER_MALE)
return HPlusConstants.ARG_GENDER_MALE;
return HPlusConstants.ARG_GENDER_FEMALE;
}
public static int getGoal(String address) {
ActivityUser activityUser = new ActivityUser();
return activityUser.getStepsGoal();
}
public static byte getScreenTime(String address) {
return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_SCREENTIME, 5) & 0xFF);
}
public static byte getAllDayHR(String address) {
Boolean value = (prefs.getBoolean(HPlusConstants.PREF_HPLUS_ALLDAYHR, true));
if(value){
return HPlusConstants.ARG_HEARTRATE_ALLDAY_ON;
}else{
return HPlusConstants.ARG_HEARTRATE_ALLDAY_OFF;
}
}
public static byte getSocial(String address) {
//TODO: Figure what this is. Returning the default value
return (byte) 255;
}
public static byte getUserWrist(String address) {
String value = prefs.getString(HPlusConstants.PREF_HPLUS_WRIST, getContext().getString(R.string.left));
if(value.equals(getContext().getString(R.string.left))){
return HPlusConstants.ARG_WRIST_LEFT;
}else{
return HPlusConstants.ARG_WRIST_RIGHT;
}
}
public static int getSITStartTime(String address) {
return prefs.getInt(HPlusConstants.PREF_HPLUS_SIT_START_TIME, 0);
}
public static int getSITEndTime(String address) {
return prefs.getInt(HPlusConstants.PREF_HPLUS_SIT_END_TIME, 0);
}
}

View File

@ -0,0 +1,223 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
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.HPlusHealthActivityOverlay;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlayDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusDataRecord;
public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealthActivitySample> {
private GBDevice mDevice;
private DaoSession mSession;
public HPlusHealthSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
mSession = session;
mDevice = device;
}
public int getID() {
return SampleProvider.PROVIDER_HPLUS;
}
public int normalizeType(int rawType) {
switch (rawType){
case HPlusDataRecord.TYPE_DAY_SLOT:
case HPlusDataRecord.TYPE_DAY_SUMMARY:
case HPlusDataRecord.TYPE_REALTIME:
case HPlusDataRecord.TYPE_SLEEP:
case HPlusDataRecord.TYPE_UNKNOWN:
return ActivityKind.TYPE_UNKNOWN;
default:
return rawType;
}
}
public int toRawActivityKind(int activityKind) {
switch (activityKind){
case ActivityKind.TYPE_DEEP_SLEEP:
return ActivityKind.TYPE_DEEP_SLEEP;
case ActivityKind.TYPE_LIGHT_SLEEP:
return ActivityKind.TYPE_LIGHT_SLEEP;
default:
return HPlusDataRecord.TYPE_DAY_SLOT;
}
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return HPlusHealthActivitySampleDao.Properties.Timestamp;
}
@Override
public HPlusHealthActivitySample createActivitySample() {
return new HPlusHealthActivitySample();
}
@Override
protected Property getRawKindSampleProperty() {
return null; // HPlusHealthActivitySampleDao.Properties.RawKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / (float) 100.0;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return HPlusHealthActivitySampleDao.Properties.DeviceId;
}
@Override
public AbstractDao<HPlusHealthActivitySample, ?> getSampleDao() {
return getSession().getHPlusHealthActivitySampleDao();
}
public List<HPlusHealthActivitySample> getActivityamples(int timestamp_from, int timestamp_to) {
return getAllActivitySamples(timestamp_from, timestamp_to);
}
public List<HPlusHealthActivitySample> getSleepSamples(int timestamp_from, int timestamp_to) {
return getAllActivitySamples(timestamp_from, timestamp_to);
}
@NonNull
@Override
public List<HPlusHealthActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to) {
List<HPlusHealthActivitySample> samples = super.getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
return Collections.emptyList();
}
QueryBuilder<HPlusHealthActivityOverlay> qb = getSession().getHPlusHealthActivityOverlayDao().queryBuilder();
qb.where(HPlusHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()),
HPlusHealthActivityOverlayDao.Properties.TimestampFrom.ge(timestamp_from - 3600 * 24),
HPlusHealthActivityOverlayDao.Properties.TimestampTo.le(timestamp_to),
HPlusHealthActivityOverlayDao.Properties.TimestampTo.ge(timestamp_from));
List<HPlusHealthActivityOverlay> overlayRecords = qb.build().list();
//Todays sample steps will come from the Day Slots messages
//Historical steps will be provided by Day Summaries messages
//This will allow both week and current day results to be consistent
Calendar today = GregorianCalendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
int stepsTodayMax = 0;
int stepsTodayCount = 0;
HPlusHealthActivitySample lastSample = null;
for(HPlusHealthActivitySample sample: samples){
if(sample.getTimestamp() >= today.getTimeInMillis() / 1000){
/**Strategy is:
* Calculate max steps from realtime messages
* Calculate sum of steps from day 10 minute slot summaries
*/
if(sample.getRawKind() == HPlusDataRecord.TYPE_REALTIME) {
stepsTodayMax = Math.max(stepsTodayMax, sample.getSteps());
}else if(sample.getRawKind() == HPlusDataRecord.TYPE_DAY_SLOT) {
stepsTodayCount += sample.getSteps();
}
sample.setSteps(ActivitySample.NOT_MEASURED);
lastSample = sample;
}else{
if (sample.getRawKind() != HPlusDataRecord.TYPE_DAY_SUMMARY) {
sample.setSteps(ActivitySample.NOT_MEASURED);
}
}
}
if(lastSample != null)
lastSample.setSteps(Math.max(stepsTodayCount, stepsTodayMax));
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
//Create fake events to improve activity counters if there are no events around the overlay
//timestamp boundaries
//Insert one before, one at the beginning, one at the end, and one 1s after.
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom() - 1, timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo(), timestamp_to), overlay.getDeviceId(), overlay.getUserId());
for (HPlusHealthActivitySample sample : samples) {
if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) {
sample.setRawKind(overlay.getRawKind());
}
}
}
detachFromSession();
Collections.sort(samples, new Comparator<HPlusHealthActivitySample>() {
public int compare(HPlusHealthActivitySample one, HPlusHealthActivitySample other) {
return one.getTimestamp() - other.getTimestamp();
}
});
return samples;
}
private List<HPlusHealthActivitySample> insertVirtualItem(List<HPlusHealthActivitySample> samples, int timestamp, long deviceId, long userId) {
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
timestamp, // ts
deviceId,
userId, // User id
null, // Raw Data
ActivityKind.TYPE_UNKNOWN,
1, // Intensity
ActivitySample.NOT_MEASURED, // Steps
ActivitySample.NOT_MEASURED, // HR
ActivitySample.NOT_MEASURED, // Distance
ActivitySample.NOT_MEASURED // Calories
);
sample.setProvider(this);
samples.add(sample);
return samples;
}
}

View File

@ -0,0 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import android.support.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
/**
* Pseudo Coordinator for the Makibes F68, a sub type of the HPLUS devices
*/
public class MakibesF68Coordinator extends HPlusCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if(name != null && name.startsWith("SPORT")){
return DeviceType.MAKIBESF68;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.MAKIBESF68;
}
@Override
public String getManufacturer() {
return "Makibes";
}
}

View File

@ -72,6 +72,11 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;

View File

@ -15,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
/**
* Also see Mi1SFirmwareInfo.
@ -26,12 +27,13 @@ public abstract class AbstractMiBandFWHelper {
private final byte[] fw;
public AbstractMiBandFWHelper(Uri uri, Context context) throws IOException {
UriHelper uriHelper = UriHelper.get(uri, context);
String pebblePattern = ".*\\.(pbw|pbz|pbl)";
if (uri.getPath().matches(pebblePattern)) {
if (uriHelper.getFileName().matches(pebblePattern)) {
throw new IOException("Firmware has a filename that looks like a Pebble app/firmware.");
}
try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) {
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB
determineFirmwareInfo(fw);
} catch (IOException ex) {

View File

@ -54,12 +54,12 @@ public class MiBand2Coordinator extends MiBandCoordinator {
// and a heuristic for now
try {
BluetoothDevice device = candidate.getDevice();
if (isHealthWearable(device)) {
// 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);
}
@ -106,4 +106,9 @@ public class MiBand2Coordinator extends MiBandCoordinator {
MiBand2FWInstallHandler handler = new MiBand2FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
}

View File

@ -31,7 +31,7 @@ public class MiBand2Service {
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 UUID UUID_CHARACTERISTIC_10_BUTTON = UUID.fromString("00000010-0000-3512-2118-0009af100700");
public static final int ALERT_LEVEL_NONE = 0;
public static final int ALERT_LEVEL_MESSAGE = 1;

View File

@ -19,9 +19,11 @@ public final class MiBandConst {
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 PREF_MIBAND_SETUP_BT_PAIRING = "mi_setup_bt_pairing";
public static final String ORIGIN_INCOMING_CALL = "incoming_call";
public static final String ORIGIN_ALARM_CLOCK = "alarm_clock";
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";

View File

@ -124,6 +124,11 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;

View File

@ -23,6 +23,9 @@ 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.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -30,11 +33,11 @@ public class MiBandPairingActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(MiBandPairingActivity.class);
private static final int REQ_CODE_USER_SETTINGS = 52;
private static final String STATE_MIBAND_ADDRESS = "mibandMacAddress";
private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate";
private static final long DELAY_AFTER_BONDING = 1000; // 1s
private TextView message;
private boolean isPairing;
private String macAddress;
private GBDeviceCandidate deviceCandidate;
private String bondingMacAddress;
private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
@ -43,9 +46,9 @@ public class MiBandPairingActivity extends GBActivity {
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())) {
if (deviceCandidate.getMacAddress().equals(device.getAddress())) {
if (device.isInitialized()) {
pairingFinished(true, macAddress);
pairingFinished(true, deviceCandidate);
} else if (device.isConnecting() || device.isInitializing()) {
LOG.info("still connecting/initializing device...");
}
@ -74,7 +77,7 @@ public class MiBandPairingActivity extends GBActivity {
attemptToConnect();
} else {
LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState);
pairingFinished(false, bondingMacAddress);
pairingFinished(false, deviceCandidate);
}
}
}
@ -86,7 +89,7 @@ public class MiBandPairingActivity extends GBActivity {
new Handler(mainLooper).postDelayed(new Runnable() {
@Override
public void run() {
performPair();
performApplicationLevelPair();
}
}, DELAY_AFTER_BONDING);
}
@ -98,11 +101,11 @@ public class MiBandPairingActivity extends GBActivity {
message = (TextView) findViewById(R.id.miband_pair_message);
Intent intent = getIntent();
macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
if (macAddress == null && savedInstanceState != null) {
macAddress = savedInstanceState.getString(STATE_MIBAND_ADDRESS, null);
deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
if (deviceCandidate == null && savedInstanceState != null) {
deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
}
if (macAddress == null) {
if (deviceCandidate == null) {
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
finish();
@ -122,13 +125,13 @@ public class MiBandPairingActivity extends GBActivity {
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(STATE_MIBAND_ADDRESS, macAddress);
outState.putParcelable(STATE_DEVICE_CANDIDATE, deviceCandidate);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
macAddress = savedInstanceState.getString(STATE_MIBAND_ADDRESS, macAddress);
deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
}
@Override
@ -145,13 +148,9 @@ public class MiBandPairingActivity extends GBActivity {
@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
}
// just to be sure, remove the receivers -- might actually be already unregistered
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
AndroidUtils.safeUnregisterBroadcastReceiver(this, mBondingReceiver);
if (isPairing) {
stopPairing();
}
@ -160,22 +159,30 @@ public class MiBandPairingActivity extends GBActivity {
private void startPairing() {
isPairing = true;
message.setText(getString(R.string.pairing, macAddress));
message.setText(getString(R.string.pairing, deviceCandidate));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
if (!shouldSetupBTLevelPairing()) {
// there are connection problems on certain Galaxy S devices at least;
// try to connect without BT pairing (bonding)
attemptToConnect();
return;
}
filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(mBondingReceiver, filter);
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
if (device != null) {
performBluetoothPair(device);
} else {
GB.toast(this, "No such Bluetooth Device: " + macAddress, Toast.LENGTH_LONG, GB.ERROR);
}
performBluetoothPair(deviceCandidate);
}
private void pairingFinished(boolean pairedSuccessfully, String macAddress) {
private boolean shouldSetupBTLevelPairing() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getPreferences().getBoolean(MiBandConst.PREF_MIBAND_SETUP_BT_PAIRING, true);
}
private void pairingFinished(boolean pairedSuccessfully, GBDeviceCandidate candidate) {
LOG.debug("pairingFinished: " + pairedSuccessfully);
if (!isPairing) {
// already gone?
@ -183,13 +190,14 @@ public class MiBandPairingActivity extends GBActivity {
}
isPairing = false;
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
unregisterReceiver(mBondingReceiver);
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
AndroidUtils.safeUnregisterBroadcastReceiver(this, mBondingReceiver);
if (pairedSuccessfully) {
// 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.
String macAddress = deviceCandidate.getMacAddress();
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) {
Prefs prefs = GBApplication.getPrefs();
@ -206,11 +214,13 @@ public class MiBandPairingActivity extends GBActivity {
isPairing = false;
}
protected void performBluetoothPair(BluetoothDevice device) {
protected void performBluetoothPair(GBDeviceCandidate deviceCandidate) {
BluetoothDevice device = deviceCandidate.getDevice();
int bondState = device.getBondState();
if (bondState == BluetoothDevice.BOND_BONDED) {
GB.toast(getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
performPair();
performApplicationLevelPair();
return;
}
@ -226,8 +236,13 @@ public class MiBandPairingActivity extends GBActivity {
}
}
private void performPair() {
private void performApplicationLevelPair() {
GBApplication.deviceService().disconnect(); // just to make sure...
GBApplication.deviceService().connect(macAddress, true);
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
if (device != null) {
GBApplication.deviceService().connect(device, true);
} else {
GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR);
}
}
}

View File

@ -17,6 +17,7 @@ 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_ALARM_CLOCK;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_INCOMING_CALL;
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;
@ -25,6 +26,7 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PR
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_SETUP_BT_PAIRING;
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.getNotificationPrefKey;
@ -145,6 +147,7 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
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_ALARM_CLOCK));
prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL));
for (NotificationType type : NotificationType.values()) {

View File

@ -60,6 +60,10 @@ public class PBWInstallHandler implements InstallHandler {
installActivity.setInfoText("file not found");
installActivity.setInstallEnabled(false);
return;
} catch (IOException e) {
installActivity.setInfoText("error reading file");
installActivity.setInstallEnabled(false);
return;
}
if (!mPBWReader.isValid()) {
@ -168,18 +172,22 @@ public class PBWInstallHandler implements InstallHandler {
}
InputStream jsConfigFile = mPBWReader.getInputStreamFile("pebble-js-app.js");
if (jsConfigFile != null) {
outputFile = new File(destDir, app.getUUID().toString() + "_config.js");
try {
outputFile = new File(destDir, app.getUUID().toString() + "_config.js");
FileUtils.copyStreamToFile(jsConfigFile, outputFile);
} catch (IOException e) {
LOG.error("Failed to open output file: " + e.getMessage(), e);
} finally {
try {
jsConfigFile.close();
} catch (IOException e) {
}
}
}
}
@Override
public boolean isValid() {
// always pretend it is valid, as we can't know yet about hw/fw version
return true;

View File

@ -1,6 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
@ -9,9 +8,7 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@ -26,6 +23,7 @@ import java.util.zip.ZipInputStream;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class PBWReader {
private static final Logger LOG = LoggerFactory.getLogger(PBWReader.class);
@ -45,8 +43,7 @@ public class PBWReader {
fwFileTypesMap.put("resources", PebbleProtocol.PUTBYTES_TYPE_SYSRESOURCES);
}
private final Uri uri;
private final ContentResolver cr;
private final UriHelper uriHelper;
private GBDeviceApp app;
private ArrayList<PebbleInstallable> pebbleInstallables = null;
private boolean isFirmware = false;
@ -60,100 +57,45 @@ public class PBWReader {
private JSONObject mAppKeys = null;
public PBWReader(Uri uri, Context context, String platform) throws FileNotFoundException {
this.uri = uri;
cr = context.getContentResolver();
public PBWReader(Uri uri, Context context, String platform) throws IOException {
uriHelper = UriHelper.get(uri, context);
InputStream fin = new BufferedInputStream(cr.openInputStream(uri));
if (uri.toString().endsWith(".pbl")) {
if (uriHelper.getFileName().endsWith(".pbl")) {
STM32CRC stm32crc = new STM32CRC();
try {
try (InputStream fin = uriHelper.openInputStream()) {
byte[] buf = new byte[2000];
while (fin.available() > 0) {
int count = fin.read(buf);
stm32crc.addData(buf, count);
}
fin.close();
} catch (IOException e) {
e.printStackTrace();
return;
}
int crc = stm32crc.getResult();
// language file
app = new GBDeviceApp(UUID.randomUUID(), "Language File", "unknown", "unknown", GBDeviceApp.Type.UNKNOWN);
File f = new File(uri.getPath());
pebbleInstallables = new ArrayList<>();
pebbleInstallables.add(new PebbleInstallable("lang", (int) f.length(), crc, PebbleProtocol.PUTBYTES_TYPE_FILE));
pebbleInstallables.add(new PebbleInstallable("lang", (int) uriHelper.getFileSize(), crc, PebbleProtocol.PUTBYTES_TYPE_FILE));
isValid = true;
isLanguage = true;
return;
}
String platformDir = "";
if (!uri.toString().endsWith(".pbz")) {
/*
* 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
*/
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(dir)) {
platformDir = dir;
break;
}
}
zis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (platform.equals("chalk") && platformDir.equals("")) {
return;
}
String platformDir = determinePlatformDir(uriHelper, platform);
if (platform.equals("chalk") && platformDir.equals("")) {
return;
}
LOG.info("using platformdir: '" + platformDir + "'");
String appName = null;
String appCreator = null;
String appVersion = null;
UUID appUUID = null;
ZipInputStream zis = new ZipInputStream(fin);
ZipEntry ze;
pebbleInstallables = new ArrayList<>();
byte[] buffer = new byte[1024];
int count;
try {
try (ZipInputStream zis = new ZipInputStream(uriHelper.openInputStream())) {
while ((ze = zis.getNextEntry()) != null) {
String fileName = ze.getName();
if (fileName.equals(platformDir + "manifest.json")) {
@ -203,8 +145,11 @@ public class PBWReader {
} else if (fileName.equals("appinfo.json")) {
long bytes = ze.getSize();
if (bytes > 65536) // that should be too much
if (bytes > 500000) {
LOG.warn(fileName + " exeeds maximum of 500000 bytes");
// that should be too much
break;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((count = zis.read(buffer)) != -1) {
@ -249,7 +194,6 @@ public class PBWReader {
// more follows but, not interesting for us
}
}
zis.close();
if (appUUID != null && appName != null && appCreator != null && appVersion != null) {
GBDeviceApp.Type appType = GBDeviceApp.Type.APP_GENERIC;
@ -260,11 +204,61 @@ public class PBWReader {
}
app = new GBDeviceApp(appUUID, appName, appCreator, appVersion, appType);
}
} catch (IOException e) {
e.printStackTrace();
else if (!isFirmware) {
isValid = false;
}
}
}
/**
* Determines the platform dir to use for the given uri and platform.
* @param uriHelper
* @param platform
* @return the platform dir to use
* @throws IOException
*/
private String determinePlatformDir(UriHelper uriHelper, String platform) throws IOException {
String platformDir = "";
if (uriHelper.getFileName().endsWith(".pbz")) {
return platformDir;
}
/*
* 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
*/
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) {
try (ZipInputStream zis = new ZipInputStream(uriHelper.openInputStream())) {
ZipEntry ze;
while ((ze = zis.getNextEntry()) != null) {
if (ze.getName().startsWith(dir)) {
return dir;
}
}
}
}
return platformDir;
}
public boolean isFirmware() {
return isFirmware;
}
@ -282,28 +276,29 @@ public class PBWReader {
}
public InputStream getInputStreamFile(String filename) {
InputStream fin;
try {
fin = new BufferedInputStream(cr.openInputStream(uri));
if (isLanguage) {
return fin;
if (isLanguage) {
try {
return uriHelper.openInputStream();
} catch (FileNotFoundException e) {
LOG.warn("file not found: " + e);
return null;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
ZipInputStream zis = new ZipInputStream(fin);
ZipInputStream zis = null;
ZipEntry ze;
try {
zis = new ZipInputStream(uriHelper.openInputStream());
while ((ze = zis.getNextEntry()) != null) {
if (ze.getName().equals(filename)) {
return zis;
return zis; // return WITHOUT closing the stream!
}
}
zis.close();
} catch (Throwable e) {
try {
zis.close();
if (zis != null) {
zis.close();
}
} catch (IOException e1) {
// ignore
}

View File

@ -108,6 +108,11 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return PebbleUtils.hasHRM(device.getModel());

View File

@ -45,8 +45,8 @@ public class PebbleHealthSampleProvider extends AbstractSampleProvider<PebbleHea
QueryBuilder<PebbleHealthActivityOverlay> 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));
qb.where(PebbleHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()), PebbleHealthActivityOverlayDao.Properties.TimestampTo.ge(timestamp_from))
.where(PebbleHealthActivityOverlayDao.Properties.TimestampFrom.le(timestamp_to));
List<PebbleHealthActivityOverlay> overlayRecords = qb.build().list();
for (PebbleHealthActivityOverlay overlay : overlayRecords) {

View File

@ -7,6 +7,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.TextView;
import android.widget.Toast;
@ -28,6 +29,7 @@ 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.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -90,7 +92,10 @@ public class PebblePairingActivity extends GBActivity {
message = (TextView) findViewById(R.id.pebble_pair_message);
Intent intent = getIntent();
macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
GBDeviceCandidate candidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
if (candidate != null) {
macAddress = candidate.getMacAddress();
}
if (macAddress == null) {
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
returnToPairingActivity();

View File

@ -73,6 +73,11 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;

View File

@ -0,0 +1,69 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
public class AlarmClockReceiver extends BroadcastReceiver {
/**
* AlarmActivity and AlarmService (when unbound) listen for this broadcast intent
* so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before
* ALARM_DONE_ACTION).
*/
public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
/**
* AlarmActivity and AlarmService listen for this broadcast intent so that other
* applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
*/
public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
/** A public action sent by AlarmService when the alarm has started. */
public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT";
/** A public action sent by AlarmService when the alarm has stopped for any reason. */
public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE";
private int lastId;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ALARM_ALERT_ACTION.equals(action)) {
sendAlarm(true);
} else if (ALARM_DONE_ACTION.equals(action)) {
sendAlarm(false);
}
}
private synchronized void sendAlarm(boolean on) {
dismissLastAlarm();
if (on) {
lastId = generateId();
NotificationSpec spec = new NotificationSpec();
spec.type = NotificationType.GENERIC_ALARM_CLOCK;
spec.id = lastId;
spec.sourceName = "ALARMCLOCKRECEIVER";
// can we get the alarm title somehow?
GBApplication.deviceService().onNotification(spec);
}
}
private void dismissLastAlarm() {
if (lastId != 0) {
GBApplication.deviceService().onDeleteNotification(lastId);
lastId = 0;
}
}
private int generateId() {
// lacks negative values, but should be sufficient
return (int) (Math.random() * Integer.MAX_VALUE);
}
}

View File

@ -0,0 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class AutoStartReceiver extends BroadcastReceiver {
private static final String TAG = AutoStartReceiver.class.getName();
@Override
public void onReceive(Context context, Intent intent) {
if (GBApplication.getGBPrefs().getAutoStart()) {
Log.i(TAG, "Boot completed, starting Gadgetbridge");
GBApplication.deviceService().start();
}
}
}

View File

@ -6,11 +6,16 @@ import android.content.Context;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class BluetoothStateChangeReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(BluetoothStateChangeReceiver.class);
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
@ -26,6 +31,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
return;
}
LOG.info("Bluetooth turned on => connecting...");
GBApplication.deviceService().connect();
} else if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_OFF) {
GBApplication.quit();

View File

@ -1,87 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.PowerManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class K9Receiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(K9Receiver.class);
private final Uri k9Uri = Uri.parse("content://com.fsck.k9.messageprovider/inbox_messages");
@Override
public void onReceive(Context context, Intent intent) {
Prefs prefs = GBApplication.getPrefs();
if ("never".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) {
return;
}
if ("when_screen_off".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) {
PowerManager powermanager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (powermanager.isScreenOn()) {
return;
}
}
switch (GBApplication.getGrantedInterruptionFilter()) {
case NotificationManager.INTERRUPTION_FILTER_ALL:
break;
case NotificationManager.INTERRUPTION_FILTER_ALARMS:
case NotificationManager.INTERRUPTION_FILTER_NONE:
case NotificationManager.INTERRUPTION_FILTER_PRIORITY:
return;
}
String uriWanted = intent.getData().toString();
String[] messagesProjection = {
"senderAddress",
"subject",
"preview",
"uri"
};
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = -1;
notificationSpec.type = NotificationType.GENERIC_EMAIL;
/*
* 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,
*/
try (Cursor c = context.getContentResolver().query(k9Uri, messagesProjection, null, null, null)) {
if (c != null) {
while (c.moveToNext()) {
String uri = c.getString(c.getColumnIndex("uri"));
if (uri.equals(uriWanted)) {
notificationSpec.sender = c.getString(c.getColumnIndex("senderAddress"));
notificationSpec.subject = c.getString(c.getColumnIndex("subject"));
notificationSpec.body = c.getString(c.getColumnIndex("preview"));
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
notificationSpec.sender = "Gadgetbridge";
notificationSpec.subject = "Permission Error?";
notificationSpec.body = "Please reinstall Gadgetbridge to enable K-9 Mail notifications";
}
GBApplication.deviceService().onNotification(notificationSpec);
}
}

View File

@ -3,6 +3,7 @@ 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;
@ -14,7 +15,7 @@ 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();
private static MusicStateSpec lastStateSpec = new MusicStateSpec();
@Override
public void onReceive(Context context, Intent intent) {
@ -26,31 +27,68 @@ public class MusicPlaybackReceiver extends BroadcastReceiver {
value != null ? value.toString() : "null", value != null ? value.getClass().getName() : "no class"));
}
*/
MusicSpec musicSpec = new MusicSpec();
musicSpec.artist = intent.getStringExtra("artist");
musicSpec.album = intent.getStringExtra("album");
musicSpec.track = intent.getStringExtra("track");
musicSpec.duration = intent.getIntExtra("duration", 0) / 1000;
MusicSpec musicSpec = new MusicSpec(lastMusicSpec);
MusicStateSpec stateSpec = new MusicStateSpec(lastStateSpec);
Bundle incomingBundle = intent.getExtras();
for (String key : incomingBundle.keySet()) {
Object incoming = incomingBundle.get(key);
if (incoming instanceof String && "artist".equals(key)) {
musicSpec.artist = (String) incoming;
} else if (incoming instanceof String && "album".equals(key)) {
musicSpec.album = (String) incoming;
} else if (incoming instanceof String && "track".equals(key)) {
musicSpec.track = (String) incoming;
} else if (incoming instanceof String && "title".equals(key) && musicSpec.track == null) {
musicSpec.track = (String) incoming;
} else if (incoming instanceof Integer && "duration".equals(key)) {
musicSpec.duration = (Integer) incoming / 1000;
} else if (incoming instanceof Long && "duration".equals(key)) {
musicSpec.duration = ((Long) incoming).intValue() / 1000;
} else if (incoming instanceof Integer && "position".equals(key)) {
stateSpec.position = (Integer) incoming / 1000;
} else if (incoming instanceof Long && "position".equals(key)) {
stateSpec.position = ((Long) incoming).intValue() / 1000;
} else if (incoming instanceof Boolean && "playing".equals(key)) {
stateSpec.state = (byte) (((Boolean) incoming) ? MusicStateSpec.STATE_PLAYING : MusicStateSpec.STATE_PAUSED);
stateSpec.playRate = (byte) (((Boolean) incoming) ? 100 : 0);
} else if (incoming instanceof String && "duration".equals(key)) {
musicSpec.duration = Integer.valueOf((String) incoming) / 1000;
} else if (incoming instanceof String && "trackno".equals(key)) {
musicSpec.trackNr = Integer.valueOf((String) incoming);
} else if (incoming instanceof String && "totaltrack".equals(key)) {
musicSpec.trackCount = Integer.valueOf((String) incoming);
} else if (incoming instanceof Integer && "pos".equals(key)) {
stateSpec.position = (Integer) incoming;
} else if (incoming instanceof Integer && "repeat".equals(key)) {
if ((Integer) incoming > 0) {
stateSpec.repeat = 1;
} else {
stateSpec.repeat = 0;
}
} else if (incoming instanceof Integer && "shuffle".equals(key)) {
if ((Integer) incoming > 0) {
stateSpec.shuffle = 1;
} else {
stateSpec.shuffle = 0;
}
}
}
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.");
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;
if (!lastStateSpec.equals(stateSpec)) {
lastStateSpec = 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.");
}
}
}

View File

@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
@ -190,19 +191,13 @@ public class NotificationListener extends NotificationListenerService {
if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) {
PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE);
if (powermanager.isScreenOn()) {
LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this");
// LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this");
return;
}
}
//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) {
LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set");
return;
}
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set");
// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags);
return;
}
@ -219,12 +214,6 @@ public class NotificationListener extends NotificationListenerService {
return;
}
if (source.equals("com.fsck.k9")) {
if (!"never".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) {
return;
}
}
if (source.equals("com.moez.QKSMS") ||
source.equals("com.android.mms") ||
source.equals("com.sonyericsson.conversations") ||
@ -258,16 +247,36 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = AppNotificationType.getInstance().get(source);
LOG.info("Processing notification from source " + source);
if (source.equals("com.fsck.k9")) {
// we dont want group summaries at all for k9
if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) {
return;
}
preferBigText = true;
}
if (notificationSpec.type == null) {
notificationSpec.type = NotificationType.UNKNOWN;
}
LOG.info("Processing notification from source " + source + " with flags: " + notification.flags);
dissectNotificationTo(notification, notificationSpec, preferBigText);
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
// ignore Gadgetbridge's very own notifications, except for those from the debug screen
if (getApplicationContext().getPackageName().equals(source)) {
if (!getApplicationContext().getString(R.string.test_notification).equals(notificationSpec.title)) {
return;
}
}
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification);
List<NotificationCompat.Action> actions = wearableExtender.getActions();
if (actions.isEmpty() && notificationSpec.type == NotificationType.TELEGRAM) {
return; // workaround for duplicate telegram message
if (actions.isEmpty() && (notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) { //this could cause #395 to come back
LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags);
return;
}
for (NotificationCompat.Action act : actions) {
@ -340,56 +349,75 @@ public class NotificationListener extends NotificationListenerService {
MediaController c;
try {
c = new MediaController(getApplicationContext(), (MediaSession.Token) extras.get(Notification.EXTRA_MEDIA_SESSION));
PlaybackState s = c.getPlaybackState();
stateSpec.position = (int) (s.getPosition() / 1000);
stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed());
stateSpec.repeat = 1;
stateSpec.shuffle = 1;
switch (s.getState()) {
case PlaybackState.STATE_PLAYING:
stateSpec.state = MusicStateSpec.STATE_PLAYING;
break;
case PlaybackState.STATE_STOPPED:
stateSpec.state = MusicStateSpec.STATE_STOPPED;
break;
case PlaybackState.STATE_PAUSED:
stateSpec.state = MusicStateSpec.STATE_PAUSED;
break;
default:
stateSpec.state = MusicStateSpec.STATE_UNKNOWN;
break;
}
MediaMetadata d = c.getMetadata();
if (d == null)
return false;
if (d.containsKey(MediaMetadata.METADATA_KEY_ARTIST))
musicSpec.artist = d.getString(MediaMetadata.METADATA_KEY_ARTIST);
if (d.containsKey(MediaMetadata.METADATA_KEY_ALBUM))
musicSpec.album = d.getString(MediaMetadata.METADATA_KEY_ALBUM);
if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE))
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);
GBApplication.deviceService().onSetMusicState(stateSpec);
return true;
} catch (NullPointerException e) {
return false;
}
PlaybackState s = c.getPlaybackState();
stateSpec.position = (int) (s.getPosition() / 1000);
stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed());
stateSpec.repeat = 1;
stateSpec.shuffle = 1;
switch (s.getState()) {
case PlaybackState.STATE_PLAYING:
stateSpec.state = MusicStateSpec.STATE_PLAYING;
break;
case PlaybackState.STATE_STOPPED:
stateSpec.state = MusicStateSpec.STATE_STOPPED;
break;
case PlaybackState.STATE_PAUSED:
stateSpec.state = MusicStateSpec.STATE_PAUSED;
break;
default:
stateSpec.state = MusicStateSpec.STATE_UNKNOWN;
break;
}
MediaMetadata d = c.getMetadata();
if (d == null)
return false;
if (d.containsKey(MediaMetadata.METADATA_KEY_ARTIST))
musicSpec.artist = d.getString(MediaMetadata.METADATA_KEY_ARTIST);
if (d.containsKey(MediaMetadata.METADATA_KEY_ALBUM))
musicSpec.album = d.getString(MediaMetadata.METADATA_KEY_ALBUM);
if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE))
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);
GBApplication.deviceService().onSetMusicState(stateSpec);
return true;
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
//FIXME: deduplicate code
String source = sbn.getPackageName();
Notification notification = sbn.getNotification();
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
return;
}
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
return;
}
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("autoremove_notifications", false)) {
LOG.info("notification removed, will ask device to delete it");
GBApplication.deviceService().onDeleteNotification((int) sbn.getPostTime()); //FIMXE: a truly unique id would be better
}
}
private void dumpExtras(Bundle bundle) {

View File

@ -0,0 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.os.Bundle;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
public class WeatherNotificationConfig extends GBActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_weather_notification);
}
}

View File

@ -0,0 +1,52 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import ru.gelin.android.weather.notification.ParcelableWeather2;
public class WeatherNotificationReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(WeatherNotificationReceiver.class);
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().contains("WEATHER_UPDATE_2")) {
LOG.info("Wrong action");
return;
}
ParcelableWeather2 weather = null;
try {
weather = intent.getParcelableExtra("ru.gelin.android.weather.notification.EXTRA_WEATHER");
} catch (RuntimeException e) {
e.printStackTrace();
}
if (weather != null) {
Weather.getInstance().setWeather2(weather);
LOG.info("weather in " + weather.location + " is " + weather.currentCondition + " (" + (weather.currentTemp - 273) + "°C)");
WeatherSpec weatherSpec = new WeatherSpec();
weatherSpec.timestamp = (int) (weather.queryTime / 1000);
weatherSpec.location = weather.location;
weatherSpec.currentTemp = weather.currentTemp;
weatherSpec.currentCondition = weather.currentCondition;
weatherSpec.currentConditionCode = weather.currentConditionCode;
weatherSpec.todayMaxTemp = weather.todayHighTemp;
weatherSpec.todayMinTemp = weather.todayLowTemp;
weatherSpec.tomorrowConditionCode = weather.forecastConditionCode;
weatherSpec.tomorrowMaxTemp = weather.forecastHighTemp;
weatherSpec.tomorrowMinTemp = weather.forecastLowTemp;
Weather.getInstance().setWeatherSpec(weatherSpec);
GBApplication.deviceService().onSendWeather(weatherSpec);
}
}
}

View File

@ -57,7 +57,7 @@ public class GBDeviceCandidate implements Parcelable {
dest.writeParcelable(device, 0);
dest.writeInt(rssi);
dest.writeString(deviceType.name());
dest.writeArray(serviceUuds);
dest.writeParcelableArray(serviceUuds, 0);
}
public static final Creator<GBDeviceCandidate> CREATOR = new Creator<GBDeviceCandidate>() {
@ -127,7 +127,7 @@ public class GBDeviceCandidate implements Parcelable {
deviceName = (String) method.invoke(device);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) {
LOG.info("Could not get device alias for " + deviceName);
LOG.info("Could not get device alias for " + device.getName());
}
if (deviceName == null || deviceName.length() == 0) {
deviceName = device.getName();
@ -167,6 +167,6 @@ public class GBDeviceCandidate implements Parcelable {
@Override
public String toString() {
return getName() + ": " + getMacAddress();
return getName() + ": " + getMacAddress() + " (" + getDeviceType() + ")";
}
}

View File

@ -3,12 +3,16 @@ package nodomain.freeyourgadget.gadgetbridge.impl;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@ -17,13 +21,30 @@ 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.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.LanguageUtils;
//import java.util.UUID;
import static nodomain.freeyourgadget.gadgetbridge.util.JavaExtensions.coalesce;
public class GBDeviceService implements DeviceService {
protected final Context mContext;
protected final Class<? extends Service> mServiceClass;
private final Class<? extends Service> mServiceClass;
private final String[] transliterationExtras = new String[]{
EXTRA_NOTIFICATION_PHONENUMBER,
EXTRA_NOTIFICATION_SENDER,
EXTRA_NOTIFICATION_SUBJECT,
EXTRA_NOTIFICATION_TITLE,
EXTRA_NOTIFICATION_BODY,
EXTRA_NOTIFICATION_SOURCENAME,
EXTRA_CALL_PHONENUMBER,
EXTRA_CALL_DISPLAYNAME,
EXTRA_MUSIC_ARTIST,
EXTRA_MUSIC_ALBUM,
EXTRA_MUSIC_TRACK,
EXTRA_CALENDAREVENT_TITLE,
EXTRA_CALENDAREVENT_DESCRIPTION
};
public GBDeviceService(Context context) {
mContext = context;
@ -35,6 +56,14 @@ public class GBDeviceService implements DeviceService {
}
protected void invokeService(Intent intent) {
if(LanguageUtils.transliterate()){
for (String extra: transliterationExtras) {
if (intent.hasExtra(extra)){
intent.putExtra(extra, LanguageUtils.transliterate(intent.getStringExtra(extra)));
}
}
}
mContext.startService(intent);
}
@ -54,21 +83,14 @@ public class GBDeviceService implements DeviceService {
}
@Override
public void connect(GBDevice device) {
Intent intent = createIntent().setAction(ACTION_CONNECT)
.putExtra(GBDevice.EXTRA_DEVICE, device);
invokeService(intent);
public void connect(@Nullable GBDevice device) {
connect(device, false);
}
@Override
public void connect(@Nullable String deviceAddress) {
connect(deviceAddress, false);
}
@Override
public void connect(@Nullable String deviceAddress, boolean performPair) {
public void connect(@Nullable GBDevice device, boolean performPair) {
Intent intent = createIntent().setAction(ACTION_CONNECT)
.putExtra(EXTRA_DEVICE_ADDRESS, deviceAddress)
.putExtra(GBDevice.EXTRA_DEVICE, device)
.putExtra(EXTRA_PERFORM_PAIR, performPair);
invokeService(intent);
}
@ -96,7 +118,7 @@ public class GBDeviceService implements DeviceService {
Intent intent = createIntent().setAction(ACTION_NOTIFICATION)
.putExtra(EXTRA_NOTIFICATION_FLAGS, notificationSpec.flags)
.putExtra(EXTRA_NOTIFICATION_PHONENUMBER, notificationSpec.phoneNumber)
.putExtra(EXTRA_NOTIFICATION_SENDER, notificationSpec.sender)
.putExtra(EXTRA_NOTIFICATION_SENDER, coalesce(notificationSpec.sender, getContactDisplayNameByNumber(notificationSpec.phoneNumber)))
.putExtra(EXTRA_NOTIFICATION_SUBJECT, notificationSpec.subject)
.putExtra(EXTRA_NOTIFICATION_TITLE, notificationSpec.title)
.putExtra(EXTRA_NOTIFICATION_BODY, notificationSpec.body)
@ -106,6 +128,14 @@ public class GBDeviceService implements DeviceService {
invokeService(intent);
}
@Override
public void onDeleteNotification(int id) {
Intent intent = createIntent().setAction(ACTION_DELETE_NOTIFICATION)
.putExtra(EXTRA_NOTIFICATION_ID, id);
invokeService(intent);
}
@Override
public void onSetTime() {
Intent intent = createIntent().setAction(ACTION_SETTIME);
@ -121,9 +151,22 @@ public class GBDeviceService implements DeviceService {
@Override
public void onSetCallState(CallSpec callSpec) {
// name is actually ignored and provided by the service itself...
Context context = GBApplication.getContext();
String currentPrivacyMode = GBApplication.getPrefs().getString("pref_call_privacy_mode", GBApplication.getContext().getString(R.string.p_call_privacy_mode_off));
if (context.getString(R.string.p_call_privacy_mode_name).equals(currentPrivacyMode)) {
callSpec.name = callSpec.number;
}
else if (context.getString(R.string.p_call_privacy_mode_complete).equals(currentPrivacyMode)) {
callSpec.number = null;
callSpec.name = null;
}
else {
callSpec.name = coalesce(callSpec.name, getContactDisplayNameByNumber(callSpec.number));
}
Intent intent = createIntent().setAction(ACTION_CALLSTATE)
.putExtra(EXTRA_CALL_PHONENUMBER, callSpec.number)
.putExtra(EXTRA_CALL_DISPLAYNAME, callSpec.name)
.putExtra(EXTRA_CALL_COMMAND, callSpec.command);
invokeService(intent);
}
@ -293,4 +336,45 @@ public class GBDeviceService implements DeviceService {
Intent intent = createIntent().setAction(ACTION_TEST_NEW_FUNCTION);
invokeService(intent);
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
Intent intent = createIntent().setAction(ACTION_SEND_WEATHER)
.putExtra(EXTRA_WEATHER_TIMESTAMP, weatherSpec.timestamp)
.putExtra(EXTRA_WEATHER_LOCATION, weatherSpec.location)
.putExtra(EXTRA_WEATHER_CURRENTTEMP, weatherSpec.currentTemp)
.putExtra(EXTRA_WEATHER_CURRENTCONDITIONCODE, weatherSpec.currentConditionCode)
.putExtra(EXTRA_WEATHER_CURRENTCONDITION, weatherSpec.currentCondition)
.putExtra(EXTRA_WEATHER_TODAYMAXTEMP, weatherSpec.todayMaxTemp)
.putExtra(EXTRA_WEATHER_TODAYMINTEMP, weatherSpec.todayMinTemp)
.putExtra(EXTRA_WEATHER_TOMORROWMAXTEMP, weatherSpec.tomorrowMaxTemp)
.putExtra(EXTRA_WEATHER_TOMORROWMINTEMP, weatherSpec.tomorrowMinTemp)
.putExtra(EXTRA_WEATHER_TOMORROWCONDITIONCODE, weatherSpec.tomorrowConditionCode);
invokeService(intent);
}
/**
* Returns contact DisplayName by call number
* @param number contact number
* @return contact DisplayName, if found it
*/
private String getContactDisplayNameByNumber(String number) {
Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
String name = number;
if (number == null || number.equals("")) {
return name;
}
try (Cursor contactLookup = mContext.getContentResolver().query(uri, null, null, null, null)) {
if (contactLookup != null && contactLookup.getCount() > 0) {
contactLookup.moveToNext();
name = contactLookup.getString(contactLookup.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
}
} catch (SecurityException e) {
// ignore, just return name below
}
return name;
}
}

View File

@ -8,6 +8,7 @@ public class ActivityAmount {
private final int activityKind;
private short percent;
private long totalSeconds;
private long totalSteps;
public ActivityAmount(int activityKind) {
this.activityKind = activityKind;
@ -17,10 +18,18 @@ public class ActivityAmount {
totalSeconds += seconds;
}
public void addSteps(long steps) {
totalSteps += steps;
}
public long getTotalSeconds() {
return totalSeconds;
}
public long getTotalSteps() {
return totalSteps;
}
public int getActivityKind() {
return activityKind;
}

View File

@ -29,6 +29,9 @@ public class AppNotificationType extends HashMap<String, NotificationType> {
// Conversations
put("eu.siacs.conversations", NotificationType.CONVERSATIONS);
// Riot
put("im.vector.alpha", NotificationType.RIOT);
// Signal
put("org.thoughtcrime.securesms", NotificationType.SIGNAL);

View File

@ -16,6 +16,7 @@ public interface DeviceService extends EventHandler {
String ACTION_START = PREFIX + ".action.start";
String ACTION_CONNECT = PREFIX + ".action.connect";
String ACTION_NOTIFICATION = PREFIX + ".action.notification";
String ACTION_DELETE_NOTIFICATION = PREFIX + ".action.delete_notification";
String ACTION_CALLSTATE = PREFIX + ".action.callstate";
String ACTION_SETCANNEDMESSAGES = PREFIX + ".action.setcannedmessages";
String ACTION_SETTIME = PREFIX + ".action.settime";
@ -38,19 +39,14 @@ public interface DeviceService extends EventHandler {
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_SEND_WEATHER = PREFIX + ".action.send_weather";
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";
String EXTRA_NOTIFICATION_ID = "notification_id";
@ -64,6 +60,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";
String EXTRA_CALL_COMMAND = "call_command";
String EXTRA_CALL_PHONENUMBER = "call_phonenumber";
String EXTRA_CALL_DISPLAYNAME = "call_displayname";
String EXTRA_CANNEDMESSAGES = "cannedmessages";
String EXTRA_CANNEDMESSAGES_TYPE = "cannedmessages_type";
String EXTRA_MUSIC_ARTIST = "music_artist";
@ -85,6 +82,18 @@ public interface DeviceService extends EventHandler {
String EXTRA_ALARMS = "alarms";
String EXTRA_PERFORM_PAIR = "perform_pair";
String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps";
String EXTRA_WEATHER_TIMESTAMP = "weather_timestamp";
String EXTRA_WEATHER_LOCATION = "weather_location";
String EXTRA_WEATHER_CURRENTTEMP = "weather_currenttemp";
String EXTRA_WEATHER_CURRENTCONDITIONCODE = "weather_currentconditioncode";
String EXTRA_WEATHER_CURRENTCONDITION = "currentcondition";
String EXTRA_WEATHER_TODAYMAXTEMP = "weather_todaymaxtemp";
String EXTRA_WEATHER_TODAYMINTEMP = "weather_todaymintemp";
String EXTRA_WEATHER_TOMORROWMAXTEMP = "weather_tomorrowmaxtemp";
String EXTRA_WEATHER_TOMORROWMINTEMP = "weather_tomorrowmintemp";
String EXTRA_WEATHER_TOMORROWCONDITIONCODE = "weather_tomorrowconditioncode";
/**
* Use EXTRA_REALTIME_SAMPLE instead
*/
@ -103,17 +112,14 @@ 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();
void connect();
void connect(GBDevice device);
void connect(@Nullable GBDevice device);
void connect(@Nullable String deviceAddress);
void connect(@Nullable String deviceAddress, boolean performPair);
void connect(@Nullable GBDevice device, boolean performPair);
void disconnect();

View File

@ -13,6 +13,8 @@ public enum DeviceType {
MIBAND2(11),
VIBRATISSIMO(20),
LIVEVIEW(30),
HPLUS(40),
MAKIBESF68(41),
TEST(1000);
private final int key;

View File

@ -17,6 +17,19 @@ public class MusicSpec {
public int trackCount;
public int trackNr;
public MusicSpec() {
}
public MusicSpec(MusicSpec old) {
this.duration = old.duration;
this.trackCount = old.trackCount;
this.trackNr = old.trackNr;
this.track = old.track;
this.album = old.album;
this.artist = old.artist;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {

View File

@ -10,11 +10,23 @@ public class MusicStateSpec {
public static final int STATE_UNKNOWN = 3;
public byte state;
public int position;
public int playRate;
public int position; // Position of the current media in seconds
public int playRate; // Speed of playback, usually 0 or 100 (full speed)
public byte shuffle;
public byte repeat;
public MusicStateSpec() {
}
public MusicStateSpec(MusicStateSpec old) {
this.state = old.state;
this.position = old.position;
this.playRate = old.playRate;
this.shuffle = old.shuffle;
this.repeat = old.repeat;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
@ -31,4 +43,15 @@ public class MusicStateSpec {
this.shuffle == stateSpec.shuffle &&
this.repeat == stateSpec.repeat;
}
@Override
public int hashCode() {
int result = (int) state;
//ignore the position -- it is taken into account in equals()
//result = 31 * result + position;
result = 31 * result + playRate;
result = 31 * result + (int) shuffle;
result = 31 * result + (int) repeat;
return result;
}
}

View File

@ -13,10 +13,12 @@ public enum NotificationType {
GENERIC_SMS(PebbleIconID.GENERIC_SMS, PebbleColor.VividViolet),
FACEBOOK(PebbleIconID.NOTIFICATION_FACEBOOK, PebbleColor.Liberty),
FACEBOOK_MESSENGER(PebbleIconID.NOTIFICATION_FACEBOOK_MESSENGER, PebbleColor.VeryLightBlue),
RIOT(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.LavenderIndigo),
SIGNAL(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.BlueMoon),
TWITTER(PebbleIconID.NOTIFICATION_TWITTER, PebbleColor.BlueMoon),
TELEGRAM(PebbleIconID.NOTIFICATION_TELEGRAM, PebbleColor.PictonBlue),
WHATSAPP(PebbleIconID.NOTIFICATION_WHATSAPP, PebbleColor.MayGreen);
WHATSAPP(PebbleIconID.NOTIFICATION_WHATSAPP, PebbleColor.MayGreen),
GENERIC_ALARM_CLOCK(PebbleIconID.ALARM_CLOCK, PebbleColor.Red);
public int icon;
public byte color;
@ -40,12 +42,14 @@ public enum NotificationType {
case GENERIC_EMAIL:
case GENERIC_NAVIGATION:
case GENERIC_SMS:
case GENERIC_ALARM_CLOCK:
return getFixedValue();
case FACEBOOK:
case TWITTER:
return "generic_social";
case CONVERSATIONS:
case FACEBOOK_MESSENGER:
case RIOT:
case SIGNAL:
case TELEGRAM:
case WHATSAPP:

View File

@ -0,0 +1,351 @@
package nodomain.freeyourgadget.gadgetbridge.model;
import ru.gelin.android.weather.notification.ParcelableWeather2;
public class Weather {
private ParcelableWeather2 weather2 = null;
private WeatherSpec weatherSpec = null;
public ParcelableWeather2 getWeather2() {
return weather2;
}
public void setWeather2(ParcelableWeather2 weather2) {
this.weather2 = weather2;
}
public WeatherSpec getWeatherSpec() {
return weatherSpec;
}
public void setWeatherSpec(WeatherSpec weatherSpec) {
this.weatherSpec = weatherSpec;
}
private static final Weather weather = new Weather();
public static Weather getInstance() {return weather;}
public static byte mapToPebbleCondition(int openWeatherMapCondition) {
/* deducted values:
0 = sun + cloud
1 = clouds
2 = some snow
3 = some rain
4 = heavy rain
5 = heavy snow
6 = sun + cloud + rain (default icon?)
7 = sun
8 = rain + snow
9 = 6
10, 11, ... = empty icon
*/
switch (openWeatherMapCondition) {
//Group 2xx: Thunderstorm
case 200: //thunderstorm with light rain: //11d
case 201: //thunderstorm with rain: //11d
case 202: //thunderstorm with heavy rain: //11d
case 210: //light thunderstorm:: //11d
case 211: //thunderstorm: //11d
case 230: //thunderstorm with light drizzle: //11d
case 231: //thunderstorm with drizzle: //11d
case 232: //thunderstorm with heavy drizzle: //11d
case 212: //heavy thunderstorm: //11d
case 221: //ragged thunderstorm: //11d
return 4;
//Group 3xx: Drizzle
case 300: //light intensity drizzle: //09d
case 301: //drizzle: //09d
case 302: //heavy intensity drizzle: //09d
case 310: //light intensity drizzle rain: //09d
case 311: //drizzle rain: //09d
case 312: //heavy intensity drizzle rain: //09d
case 313: //shower rain and drizzle: //09d
case 314: //heavy shower rain and drizzle: //09d
case 321: //shower drizzle: //09d
case 500: //light rain: //10d
case 501: //moderate rain: //10d
return 3;
//Group 5xx: Rain
case 502: //heavy intensity rain: //10d
case 503: //very heavy rain: //10d
case 504: //extreme rain: //10d
case 511: //freezing rain: //13d
case 520: //light intensity shower rain: //09d
case 521: //shower rain: //09d
case 522: //heavy intensity shower rain: //09d
case 531: //ragged shower rain: //09d
return 4;
//Group 6xx: Snow
case 600: //light snow: //[[file:13d.png]]
case 601: //snow: //[[file:13d.png]]
case 620: //light shower snow: //[[file:13d.png]]
return 2;
case 602: //heavy snow: //[[file:13d.png]]
case 611: //sleet: //[[file:13d.png]]
case 612: //shower sleet: //[[file:13d.png]]
case 621: //shower snow: //[[file:13d.png]]
case 622: //heavy shower snow: //[[file:13d.png]]
return 5;
case 615: //light rain and snow: //[[file:13d.png]]
case 616: //rain and snow: //[[file:13d.png]]
return 8;
//Group 7xx: Atmosphere
case 701: //mist: //[[file:50d.png]]
case 711: //smoke: //[[file:50d.png]]
case 721: //haze: //[[file:50d.png]]
case 731: //sandcase dust whirls: //[[file:50d.png]]
case 741: //fog: //[[file:50d.png]]
case 751: //sand: //[[file:50d.png]]
case 761: //dust: //[[file:50d.png]]
case 762: //volcanic ash: //[[file:50d.png]]
case 771: //squalls: //[[file:50d.png]]
case 781: //tornado: //[[file:50d.png]]
case 900: //tornado
return 6;
//Group 800: Clear
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
return 7;
//Group 80x: Clouds
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
return 0;
//Group 90x: Extreme
case 901: //tropical storm
case 903: //cold
case 904: //hot
case 905: //windy
case 906: //hail
//Group 9xx: Additional
case 951: //calm
case 952: //light breeze
case 953: //gentle breeze
case 954: //moderate breeze
case 955: //fresh breeze
case 956: //strong breeze
case 957: //high windcase near gale
case 958: //gale
case 959: //severe gale
case 960: //storm
case 961: //violent storm
case 902: //hurricane
case 962: //hurricane
default:
return 6;
}
}
public static int mapToYahooCondition(int openWeatherMapCondition) {
// openweathermap.org conditions:
// http://openweathermap.org/weather-conditions
switch (openWeatherMapCondition) {
//Group 2xx: Thunderstorm
case 200: //thunderstorm with light rain: //11d
case 201: //thunderstorm with rain: //11d
case 202: //thunderstorm with heavy rain: //11d
case 210: //light thunderstorm:: //11d
case 211: //thunderstorm: //11d
case 230: //thunderstorm with light drizzle: //11d
case 231: //thunderstorm with drizzle: //11d
case 232: //thunderstorm with heavy drizzle: //11d
return 4;
case 212: //heavy thunderstorm: //11d
case 221: //ragged thunderstorm: //11d
return 3;
//Group 3xx: Drizzle
case 300: //light intensity drizzle: //09d
case 301: //drizzle: //09d
case 302: //heavy intensity drizzle: //09d
case 310: //light intensity drizzle rain: //09d
case 311: //drizzle rain: //09d
case 312: //heavy intensity drizzle rain: //09d
return 9;
case 313: //shower rain and drizzle: //09d
case 314: //heavy shower rain and drizzle: //09d
case 321: //shower drizzle: //09d
return 11;
//Group 5xx: Rain
case 500: //light rain: //10d
case 501: //moderate rain: //10d
case 502: //heavy intensity rain: //10d
case 503: //very heavy rain: //10d
case 504: //extreme rain: //10d
case 511: //freezing rain: //13d
return 10;
case 520: //light intensity shower rain: //09d
return 40;
case 521: //shower rain: //09d
case 522: //heavy intensity shower rain: //09d
case 531: //ragged shower rain: //09d
return 12;
//Group 6xx: Snow
case 600: //light snow: //[[file:13d.png]]
return 7;
case 601: //snow: //[[file:13d.png]]
return 16;
case 602: //heavy snow: //[[file:13d.png]]
return 15;
case 611: //sleet: //[[file:13d.png]]
case 612: //shower sleet: //[[file:13d.png]]
return 18;
case 615: //light rain and snow: //[[file:13d.png]]
case 616: //rain and snow: //[[file:13d.png]]
return 5;
case 620: //light shower snow: //[[file:13d.png]]
return 14;
case 621: //shower snow: //[[file:13d.png]]
return 46;
case 622: //heavy shower snow: //[[file:13d.png]]
//Group 7xx: Atmosphere
case 701: //mist: //[[file:50d.png]]
case 711: //smoke: //[[file:50d.png]]
return 22;
case 721: //haze: //[[file:50d.png]]
return 21;
case 731: //sandcase dust whirls: //[[file:50d.png]]
return 3200;
case 741: //fog: //[[file:50d.png]]
return 20;
case 751: //sand: //[[file:50d.png]]
case 761: //dust: //[[file:50d.png]]
return 19;
case 762: //volcanic ash: //[[file:50d.png]]
case 771: //squalls: //[[file:50d.png]]
return 3200;
case 781: //tornado: //[[file:50d.png]]
case 900: //tornado
return 0;
//Group 800: Clear
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
return 32;
//Group 80x: Clouds
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
return 34;
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
return 44;
//Group 90x: Extreme
case 901: //tropical storm
return 1;
case 903: //cold
return 25;
case 904: //hot
return 36;
case 905: //windy
return 24;
case 906: //hail
return 17;
//Group 9xx: Additional
case 951: //calm
case 952: //light breeze
case 953: //gentle breeze
case 954: //moderate breeze
case 955: //fresh breeze
return 34;
case 956: //strong breeze
case 957: //high windcase near gale
return 24;
case 958: //gale
case 959: //severe gale
case 960: //storm
case 961: //violent storm
return 3200;
case 902: //hurricane
case 962: //hurricane
return 2;
default:
return 3200;
}
}
public static int mapToOpenWeatherMapCondition(int yahooCondition) {
switch (yahooCondition) {
//yahoo weather conditions:
//https://developer.yahoo.com/weather/documentation.html
case 0: //tornado
return 900;
case 1: //tropical storm
return 901;
case 2: //hurricane
return 962;
case 3: //severe thunderstorms
return 212;
case 4: //thunderstorms
return 211;
case 5: //mixed rain and snow
case 6: //mixed rain and sleet
return 616;
case 7: //mixed snow and sleet
return 600;
case 8: //freezing drizzle
case 9: //drizzle
return 301;
case 10: //freezing rain
return 511;
case 11: //showers
case 12: //showers
return 521;
case 13: //snow flurries
case 14: //light snow showers
return 620;
case 15: //blowing snow
case 41: //heavy snow
case 42: //scattered snow showers
case 43: //heavy snow
case 46: //snow showers
return 602;
case 16: //snow
return 601;
case 17: //hail
case 35: //mixed rain and hail
return 906;
case 18: //sleet
return 611;
case 19: //dust
return 761;
case 20: //foggy
return 741;
case 21: //haze
return 721;
case 22: //smoky
return 711;
case 23: //blustery
case 24: //windy
return 905;
case 25: //cold
return 903;
case 26: //cloudy
case 27: //mostly cloudy (night)
case 28: //mostly cloudy (day)
return 804;
case 29: //partly cloudy (night)
case 30: //partly cloudy (day)
return 801;
case 31: //clear (night)
case 32: //sunny
return 800;
case 33: //fair (night)
case 34: //fair (day)
return 801;
case 36: //hot
return 904;
case 37: //isolated thunderstorms
case 38: //scattered thunderstorms
case 39: //scattered thunderstorms
return 210;
case 40: //scattered showers
return 520;
case 44: //partly cloudy
return 801;
case 45: //thundershowers
case 47: //isolated thundershowers
return 621;
case 3200: //not available
default:
return -1;
}
}
}

View File

@ -0,0 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.model;
public class WeatherSpec {
public int timestamp;
public String location;
public int currentTemp;
public int currentConditionCode;
public String currentCondition;
public int todayMaxTemp;
public int todayMinTemp;
public int tomorrowMaxTemp;
public int tomorrowMinTemp;
public int tomorrowConditionCode;
}

View File

@ -8,10 +8,8 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.IBinder;
import android.provider.ContactsContract;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
@ -25,13 +23,12 @@ 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.AlarmClockReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.K9Receiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.MusicPlaybackReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.PebbleReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.PhoneCallReceiver;
@ -42,11 +39,11 @@ 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;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
@ -59,6 +56,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CA
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETE_CALENDAREVENT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETE_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DISCONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT;
@ -73,6 +71,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RE
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_SEND_WEATHER;
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;
@ -94,11 +93,11 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAL
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TITLE;
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_DISPLAYNAME;
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;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST;
@ -123,6 +122,16 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOT
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;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_CURRENTCONDITION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_CURRENTCONDITIONCODE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_CURRENTTEMP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_LOCATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_TODAYMAXTEMP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_TODAYMINTEMP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_TOMORROWCONDITIONCODE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_TOMORROWMAXTEMP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_TOMORROWMINTEMP;
public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class);
@ -136,15 +145,25 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private PhoneCallReceiver mPhoneCallReceiver = null;
private SMSReceiver mSMSReceiver = null;
private K9Receiver mK9Receiver = null;
private PebbleReceiver mPebbleReceiver = null;
private MusicPlaybackReceiver mMusicPlaybackReceiver = null;
private TimeChangeReceiver mTimeChangeReceiver = null;
private BluetoothConnectReceiver mBlueToothConnectReceiver = null;
private AlarmReceiver mAlarmReceiver = null;
private AlarmClockReceiver mAlarmClockReceiver = null;
private AlarmReceiver mAlarmReceiver = null;
private Random mRandom = new Random();
private final String[] mMusicActions = {
"com.android.music.metachanged",
"com.android.music.playstatechanged",
"com.android.music.queuechanged",
"com.android.music.playbackcomplete",
"net.sourceforge.subsonic.androidapp.EVENT_META_CHANGED",
"com.maxmpz.audioplayer.TPOS_SYNC",
"com.maxmpz.audioplayer.STATUS_CHANGED",
"com.maxmpz.audioplayer.PLAYING_MODE_CHANGED"};
/**
* For testing!
*
@ -164,6 +183,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
String action = intent.getAction();
if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
// FIXME: mGBDevice was null here once
if (mGBDevice.equals(device)) {
mGBDevice = device;
boolean enableReceivers = mDeviceSupport != null && (mDeviceSupport.useAutoConnect() || mGBDevice.isInitialized());
@ -173,20 +193,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
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) {
}
}
@ -263,12 +270,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
GBDevice gbDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
String btDeviceAddress = null;
if (gbDevice == null) {
btDeviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
if (btDeviceAddress == null && prefs != null) { // may be null in test cases
if (prefs != null) { // may be null in test cases
btDeviceAddress = prefs.getString("last_device_address", null);
}
if (btDeviceAddress != null) {
gbDevice = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this);
if (btDeviceAddress != null) {
gbDevice = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this);
}
}
} else {
btDeviceAddress = gbDevice.getAddress();
@ -314,16 +320,16 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.subject = intent.getStringExtra(EXTRA_NOTIFICATION_SUBJECT);
notificationSpec.title = intent.getStringExtra(EXTRA_NOTIFICATION_TITLE);
notificationSpec.body = intent.getStringExtra(EXTRA_NOTIFICATION_BODY);
notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME);
notificationSpec.type = (NotificationType) intent.getSerializableExtra(EXTRA_NOTIFICATION_TYPE);
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.GENERIC_SMS && notificationSpec.phoneNumber != null) {
notificationSpec.sender = getContactDisplayNameByNumber(notificationSpec.phoneNumber);
if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
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.GENERIC_SMS && notificationSpec.phoneNumber != null)) {
// NOTE: maybe not where it belongs
@ -340,9 +346,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.cannedReplies = replies.toArray(new String[replies.size()]);
}
}
mDeviceSupport.onNotification(notificationSpec);
break;
}
case ACTION_DELETE_NOTIFICATION: {
mDeviceSupport.onDeleteNotification(intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1));
break;
}
case ACTION_ADD_CALENDAREVENT: {
CalendarEventSpec calendarEventSpec = new CalendarEventSpec();
calendarEventSpec.id = intent.getLongExtra(EXTRA_CALENDAREVENT_ID, -1);
@ -393,18 +404,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
break;
}
case ACTION_CALLSTATE:
int command = intent.getIntExtra(EXTRA_CALL_COMMAND, CallSpec.CALL_UNDEFINED);
String phoneNumber = intent.getStringExtra(EXTRA_CALL_PHONENUMBER);
String callerName = null;
if (phoneNumber != null) {
callerName = getContactDisplayNameByNumber(phoneNumber);
}
CallSpec callSpec = new CallSpec();
callSpec.command = command;
callSpec.number = phoneNumber;
callSpec.name = callerName;
callSpec.command = intent.getIntExtra(EXTRA_CALL_COMMAND, CallSpec.CALL_UNDEFINED);
callSpec.number = intent.getStringExtra(EXTRA_CALL_PHONENUMBER);
callSpec.name = intent.getStringExtra(EXTRA_CALL_DISPLAYNAME);
mDeviceSupport.onSetCallState(callSpec);
break;
case ACTION_SETCANNEDMESSAGES:
@ -501,6 +504,21 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mDeviceSupport.onTestNewFunction();
break;
}
case ACTION_SEND_WEATHER: {
WeatherSpec weatherSpec = new WeatherSpec();
weatherSpec.timestamp = intent.getIntExtra(EXTRA_WEATHER_TIMESTAMP, 0);
weatherSpec.location = intent.getStringExtra(EXTRA_WEATHER_LOCATION);
weatherSpec.currentTemp = intent.getIntExtra(EXTRA_WEATHER_CURRENTTEMP, 0);
weatherSpec.currentConditionCode = intent.getIntExtra(EXTRA_WEATHER_CURRENTCONDITIONCODE, 0);
weatherSpec.currentCondition = intent.getStringExtra(EXTRA_WEATHER_CURRENTCONDITION);
weatherSpec.todayMaxTemp = intent.getIntExtra(EXTRA_WEATHER_TODAYMAXTEMP, 0);
weatherSpec.todayMinTemp = intent.getIntExtra(EXTRA_WEATHER_TODAYMINTEMP, 0);
weatherSpec.tomorrowMaxTemp = intent.getIntExtra(EXTRA_WEATHER_TOMORROWMAXTEMP, 0);
weatherSpec.tomorrowMinTemp = intent.getIntExtra(EXTRA_WEATHER_TOMORROWMINTEMP, 0);
weatherSpec.tomorrowConditionCode = intent.getIntExtra(EXTRA_WEATHER_TOMORROWCONDITIONCODE, 0);
mDeviceSupport.onSendWeather(weatherSpec);
break;
}
}
return START_STICKY;
@ -561,13 +579,6 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mSMSReceiver = new SMSReceiver();
registerReceiver(mSMSReceiver, new IntentFilter("android.provider.Telephony.SMS_RECEIVED"));
}
if (mK9Receiver == null) {
mK9Receiver = new K9Receiver();
IntentFilter filter = new IntentFilter();
filter.addDataScheme("email");
filter.addAction("com.fsck.k9.intent.action.EMAIL_RECEIVED");
registerReceiver(mK9Receiver, filter);
}
if (mPebbleReceiver == null) {
mPebbleReceiver = new PebbleReceiver();
registerReceiver(mPebbleReceiver, new IntentFilter("com.getpebble.action.SEND_NOTIFICATION"));
@ -575,8 +586,9 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
if (mMusicPlaybackReceiver == null) {
mMusicPlaybackReceiver = new MusicPlaybackReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction("com.android.music.metachanged");
//filter.addAction("com.android.music.playstatechanged");
for (String action : mMusicActions){
filter.addAction(action);
}
registerReceiver(mMusicPlaybackReceiver, filter);
}
if (mTimeChangeReceiver == null) {
@ -594,6 +606,13 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mAlarmReceiver = new AlarmReceiver();
registerReceiver(mAlarmReceiver, new IntentFilter("DAILY_ALARM"));
}
if (mAlarmClockReceiver == null) {
mAlarmClockReceiver = new AlarmClockReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(AlarmClockReceiver.ALARM_ALERT_ACTION);
filter.addAction(AlarmClockReceiver.ALARM_DONE_ACTION);
registerReceiver(mAlarmClockReceiver, filter);
}
} else {
if (mPhoneCallReceiver != null) {
unregisterReceiver(mPhoneCallReceiver);
@ -603,10 +622,6 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mSMSReceiver);
mSMSReceiver = null;
}
if (mK9Receiver != null) {
unregisterReceiver(mK9Receiver);
mK9Receiver = null;
}
if (mPebbleReceiver != null) {
unregisterReceiver(mPebbleReceiver);
mPebbleReceiver = null;
@ -627,6 +642,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mAlarmReceiver);
mAlarmReceiver = null;
}
if (mAlarmClockReceiver != null) {
unregisterReceiver(mAlarmClockReceiver);
mAlarmClockReceiver = null;
}
}
}
@ -652,27 +671,6 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
return null;
}
private String getContactDisplayNameByNumber(String number) {
Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
String name = number;
if (number == null || number.equals("")) {
return name;
}
try (Cursor contactLookup = getContentResolver().query(uri, null, null, null, null)) {
if (contactLookup != null && contactLookup.getCount() > 0) {
contactLookup.moveToNext();
name = contactLookup.getString(contactLookup.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
}
} catch (SecurityException e) {
// ignore, just return name below
}
return name;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (GBPrefs.AUTO_RECONNECT.equals(key)) {

View File

@ -10,11 +10,13 @@ import java.util.EnumSet;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.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.service.devices.hplus.HPlusSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DeviceSupportFactory {
@ -96,6 +98,12 @@ public class DeviceSupportFactory {
case LIVEVIEW:
deviceSupport = new ServiceDeviceSupport(new LiveviewSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case HPLUS:
deviceSupport = new ServiceDeviceSupport(new HPlusSupport(DeviceType.HPLUS), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case MAKIBESF68:
deviceSupport = new ServiceDeviceSupport(new HPlusSupport(DeviceType.MAKIBESF68), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
}
if (deviceSupport != null) {
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);

View File

@ -19,6 +19,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
/**
* Wraps another device support instance and supports busy-checking and throttling of events.
@ -134,6 +135,11 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onNotification(notificationSpec);
}
@Override
public void onDeleteNotification(int id) {
delegate.onDeleteNotification(id);
}
@Override
public void onSetTime() {
if (checkBusy("set time") || checkThrottle("set time")) {
@ -335,4 +341,12 @@ public class ServiceDeviceSupport implements DeviceSupport {
}
delegate.onTestNewFunction();
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
if (checkBusy("send weather event")) {
return;
}
delegate.onSendWeather(weatherSpec);
}
}

View File

@ -1,7 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -178,64 +181,12 @@ public class GattCharacteristic {
public static final UUID UUID_CHARACTERISTIC_WIND_CHILL = UUID.fromString((String.format(AbstractBTLEDeviceSupport.BASE_UUID, "2A79")));
//do we need this?
private static Map<UUID, String> GATTCHARACTERISTIC_DEBUG;
private static final Map<UUID, String> GATTCHARACTERISTIC_DEBUG;
static {
GATTCHARACTERISTIC_DEBUG = new HashMap<>();
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");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_APPEARANCE, "Appearance");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_BLOOD_PRESSURE_FEATURE, "Blood Pressure Feature");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_BLOOD_PRESSURE_MEASUREMENT, "Blood Pressure Measurement");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_BODY_SENSOR_LOCATION, "Body Sensor Location");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_CURRENT_TIME, "Current Time");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DATE_TIME, "Date Time");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DAY_DATE_TIME, "Day Date Time");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DAY_OF_WEEK, "Day of Week");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_DEVICE_NAME, "Device Name");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DST_OFFSET, "DST Offset");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_EXACT_TIME_256, "Exact Time 256");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING, "Firmware Revision String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING, "Hardware Revision String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT, "Heart Rate Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT, "Heart Rate Measurement");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST, "IEEE 11073-20601 Regulatory");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_INTERMEDIATE_BLOOD_PRESSURE, "Intermediate Cuff Pressure");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_INTERMEDIATE_TEMPERATURE, "Intermediate Temperature");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_LOCAL_TIME_INFORMATION, "Local Time Information");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING, "Manufacturer Name String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL, "Measurement Interval");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING, "Model Number String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_NEW_ALERT, "New Alert");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS, "Peripheral Preferred Connection Parameters");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_PERIPHERAL_PRIVACY_FLAG, "Peripheral Privacy Flag");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_RECONNECTION_ADDRESS, "Reconnection Address");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_REFERENCE_TIME_INFORMATION, "Reference Time Information");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_RINGER_CONTROL_POINT, "Ringer Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_RINGER_SETTING, "Ringer Setting");
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 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");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_ACCURACY, "Time Accuracy");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_SOURCE, "Time Source");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_UPDATE_CONTROL_POINT, "Time Update Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_UPDATE_STATE, "Time Update State");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_WITH_DST, "Time with DST");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_ZONE, "Time Zone");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TX_POWER_LEVEL, "Tx Power Level");
}
public static String lookup(UUID uuid, String fallback) {
public static synchronized String lookup(UUID uuid, String fallback) {
if (GATTCHARACTERISTIC_DEBUG == null) {
GATTCHARACTERISTIC_DEBUG = initDebugMap();
}
String name = GATTCHARACTERISTIC_DEBUG.get(uuid);
if (name == null) {
name = fallback;
@ -243,6 +194,42 @@ public class GattCharacteristic {
return name;
}
private static Map<UUID, String> initDebugMap() {
Map<UUID,String> map = new HashMap<>();
try {
for (Field field : GattCharacteristic.class.getDeclaredFields()) {
if ((field.getModifiers() & Modifier.STATIC) != 0 && field.getType() == UUID.class) {
UUID uuid = (UUID) field.get(null);
if (uuid != null) {
map.put(uuid, toPrettyName(field.getName()));
}
}
}
} catch (Exception ex) {
Log.w(GattCharacteristic.class.getName(), "Error reading UUID fields by reflection: " + ex.getMessage(), ex);
}
return map;
}
private static String toPrettyName(String fieldName) {
String[] words = fieldName.split("_");
if (words.length <= 1) {
return fieldName.toLowerCase();
}
StringBuilder builder = new StringBuilder(fieldName.length());
for (String word : words) {
if (word.length() == 0 || "UUID".equals(word) || "CHARACTERISTIC".equals(word)) {
continue;
}
if (builder.length() > 0) {
builder.append(" ");
}
builder.append(word.toLowerCase());
}
return builder.toString();
}
public static String toString(BluetoothGattCharacteristic characteristic) {
return characteristic.getUuid() + " (" + lookup(characteristic.getUuid(), "unknown") + ")";
}

View File

@ -0,0 +1,66 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
public class HPlusDataRecord {
public final static int TYPE_UNKNOWN = 0;
public final static int TYPE_SLEEP = 100;
public final static int TYPE_DAY_SUMMARY = 101;
public final static int TYPE_DAY_SLOT = 102;
public final static int TYPE_REALTIME = 103;
public int type = TYPE_UNKNOWN;
public int activityKind = ActivityKind.TYPE_UNKNOWN;
/**
* Time of this record in seconds
*/
public int timestamp;
/**
* Raw data as sent from the device
*/
public byte[] rawData;
protected HPlusDataRecord(){
}
protected HPlusDataRecord(byte[] data, int type){
this.rawData = data;
this.type = type;
}
public byte[] getRawData() {
return rawData;
}
public class RecordInterval {
/**
* Start time of this interval in seconds
*/
public int timestampFrom;
/**
* End time of this interval in seconds
*/
public int timestampTo;
/**
* Type of activity {@link ActivityKind}
*/
public int activityKind;
RecordInterval(int timestampFrom, int timestampTo, int activityKind) {
this.timestampFrom = timestampFrom;
this.timestampTo = timestampTo;
this.activityKind = activityKind;
}
}
}

View File

@ -0,0 +1,89 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class HPlusDataRecordDaySlot extends HPlusDataRecord {
/**
* The device reports data aggregated in slots.
* There are 144 slots in a given day, summarizing 10 minutes of data
* Integer with the slot number from 0 to 143
*/
public int slot;
/**
* Number of steps
*/
public int steps;
/**
* Number of seconds without activity (TBC)
*/
public int secondsInactive;
/**
* Average Heart Rate in Beats Per Minute
*/
public int heartRate;
public HPlusDataRecordDaySlot(byte[] data) {
super(data, TYPE_DAY_SLOT);
int a = (data[4] & 0xFF) * 256 + (data[5] & 0xFF);
if (a >= 144) {
throw new IllegalArgumentException("Invalid Slot Number");
}
slot = a;
heartRate = data[1] & 0xFF;
if(heartRate == 255 || heartRate == 0)
heartRate = ActivitySample.NOT_MEASURED;
steps = (data[2] & 0xFF) * 256 + (data[3] & 0xFF);
//?? data[6]; atemp?? always 0
secondsInactive = data[7] & 0xFF; // ?
Calendar slotTime = GregorianCalendar.getInstance();
slotTime.set(Calendar.MINUTE, (slot % 6) * 10);
slotTime.set(Calendar.HOUR_OF_DAY, slot / 6);
slotTime.set(Calendar.SECOND, 0);
timestamp = (int) (slotTime.getTimeInMillis() / 1000L);
}
public String toString(){
Calendar slotTime = GregorianCalendar.getInstance();
slotTime.setTimeInMillis(timestamp * 1000L);
return String.format(Locale.US, "Slot: %d, Time: %s, Steps: %d, InactiveSeconds: %d, HeartRate: %d", slot, slotTime.getTime(), steps, secondsInactive, heartRate);
}
public void accumulate(HPlusDataRecordDaySlot other){
if(other == null)
return;
if(steps == ActivitySample.NOT_MEASURED)
steps = other.steps;
else if(other.steps != ActivitySample.NOT_MEASURED)
steps += other.steps;
if(heartRate == ActivitySample.NOT_MEASURED)
heartRate = other.heartRate;
else if(other.heartRate != ActivitySample.NOT_MEASURED) {
heartRate = (heartRate + other.heartRate) / 2;
}
secondsInactive += other.secondsInactive;
}
}

View File

@ -0,0 +1,101 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
class HPlusDataRecordDaySummary extends HPlusDataRecord{
/**
* Year of the record reported by the device
* Sometimes the device will report a low number (e.g, 116) which will be "corrected"
* by adding 1900
*/
public int year;
/**
* Month of the record reported by the device from 1 to 12
*/
public int month;
/**
* Day of the record reported by the device
*/
public int day;
/**
* Number of steps accumulated in the day reported
*/
public int steps;
/**
* Distance in meters accumulated in the day reported
*/
public int distance;
/**
* Amount of time active in the day (Units are To Be Determined)
*/
public int activeTime;
/**
* Max Heart Rate recorded in Beats Per Minute
*/
public int maxHeartRate;
/**
* Min Heart Rate recorded in Beats Per Minute
*/
public int minHeartRate;
/**
* Amount of estimated calories consumed during the day in KCalories
*/
public int calories;
HPlusDataRecordDaySummary(byte[] data) {
super(data, TYPE_DAY_SUMMARY);
year = (data[10] & 0xFF) * 256 + (data[9] & 0xFF);
month = data[11] & 0xFF;
day = data[12] & 0xFF;
//Recover from bug in firmware where year is corrupted
//data[10] will be set to 0, effectively offsetting values by minus 1900 years
if(year < 1900)
year += 1900;
if (year < 2000 || month > 12 || day > 31) {
throw new IllegalArgumentException("Invalid record date "+year+"-"+month+"-"+day);
}
steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
distance = ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)) * 10;
activeTime = (data[14] & 0xFF) * 256 + (data[13] & 0xFF);
calories = (data[6] & 0xFF) * 256 + (data[5] & 0xFF);
calories += (data[8] & 0xFF) * 256 + (data[7] & 0xFF);
maxHeartRate = data[15] & 0xFF;
minHeartRate = data[16] & 0xFF;
Calendar date = GregorianCalendar.getInstance();
date.set(Calendar.YEAR, year);
date.set(Calendar.MONTH, month - 1);
date.set(Calendar.DAY_OF_MONTH, day);
date.set(Calendar.HOUR_OF_DAY, 23);
date.set(Calendar.MINUTE, 59);
date.set(Calendar.SECOND, 59);
date.set(Calendar.MILLISECOND, 999);
timestamp = (int) (date.getTimeInMillis() / 1000);
}
public String toString(){
return String.format(Locale.US, "%s-%s-%s steps:%d distance:%d minHR:%d maxHR:%d calories:%d activeTime:%d", year, month, day, steps, distance,minHeartRate, maxHeartRate, calories, activeTime);
}
}

View File

@ -0,0 +1,114 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
class HPlusDataRecordRealtime extends HPlusDataRecord {
/**
* Distance accumulated during the day in meters
*/
public int distance;
/**
* Calories consumed during the day in KCalories
*/
public int calories;
/**
* Instantaneous Heart Rate measured in Beats Per Minute
*/
public int heartRate;
/**
* Battery level from 0 to 100
*/
public byte battery;
/**
* Number of steps today
*/
public int steps;
/**
* Time active (To be determined how it works)
*/
public int activeTime;
/**
* Computing intensity
* To be calculated appropriately
*/
public int intensity;
public HPlusDataRecordRealtime(byte[] data) {
super(data, TYPE_REALTIME);
if (data.length < 15) {
throw new IllegalArgumentException("Invalid data packet");
}
timestamp = (int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000);
distance = 10 * ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)); // meters
steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
int x = (data[6] & 0xFF) * 256 + (data[5] & 0xFF);
int y = (data[8] & 0xFF) * 256 + (data[7] & 0xFF);
battery = data[9];
calories = x + y; // KCal
heartRate = data[11] & 0xFF; // BPM
activeTime = (data[14] & 0xFF * 256) + (data[13] & 0xFF);
if(heartRate == 255) {
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_MEASURED;
heartRate = ActivitySample.NOT_MEASURED;
}
else {
intensity = (int) (100 * Math.max(0, Math.min((heartRate - 60) / 120.0, 1))); // TODO: Calculate a proper value
activityKind = ActivityKind.TYPE_UNKNOWN;
}
}
public void computeActivity(HPlusDataRecordRealtime prev){
if(prev == null)
return;
int deltaDistance = distance - prev.distance;
if(deltaDistance <= 0)
return;
int deltaTime = timestamp - prev.timestamp;
if(deltaTime <= 0)
return;
double speed = deltaDistance / deltaTime;
if(speed >= 1.6) // ~6 KM/h
activityKind = ActivityKind.TYPE_ACTIVITY;
}
public boolean same(HPlusDataRecordRealtime other){
if(other == null)
return false;
return steps == other.steps && distance == other.distance && calories == other.calories && heartRate == other.heartRate && battery == other.battery;
}
public String toString(){
return String.format(Locale.US, "Distance: %d Steps: %d Calories: %d HeartRate: %d Battery: %d ActiveTime: %d Intensity: %d", distance, steps, calories, heartRate, battery, activeTime, intensity);
}
}

View File

@ -0,0 +1,129 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class HPlusDataRecordSleep extends HPlusDataRecord {
/**
* Time which the device determined to be the bed time in seconds
*/
public int bedTimeStart;
/**
* Time which the device determined to be the end of this sleep period in seconds
*/
public int bedTimeEnd;
/**
* Number of minutes in Deep Sleep
*/
public int deepSleepMinutes;
/**
* Number of minutes in Light Sleep
* This is considered as Light Sleep
*/
public int lightSleepMinutes;
/**
* Number of minutes to start sleeping (??)
* This is considered as Light Sleep
*/
public int enterSleepMinutes;
/**
* Number of minutes with Sleep Spindles (??)
* This is considered as Light Sleep
*/
public int spindleMinutes;
/**
* Number of minutes in REM sleep
* This is considered as Light Sleep
*/
public int remSleepMinutes;
/**
* Number of wake up minutes during the sleep period
* This is not considered as a sleep activity
*/
public int wakeupMinutes;
/**
* Number of times the user woke up
*/
public int wakeupCount;
public HPlusDataRecordSleep(byte[] data) {
super(data, TYPE_SLEEP);
int year = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
int month = data[3] & 0xFF;
int day = data[4] & 0xFF;
if (year < 2000) //Attempt to recover from bug from device.
year += 1900;
if (year < 2000 || month > 12 || day <= 0 || day > 31) {
throw new IllegalArgumentException("Invalid record date: " + year + "-" + month + "-" + day);
}
enterSleepMinutes = ((data[6] & 0xFF) * 256 + (data[5] & 0xFF));
spindleMinutes = ((data[8] & 0xFF) * 256 + (data[7] & 0xFF));
deepSleepMinutes = ((data[10] & 0xFF) * 256 + (data[9] & 0xFF));
remSleepMinutes = ((data[12] & 0xFF) * 256 + (data[11] & 0xFF));
wakeupMinutes = ((data[14] & 0xFF) * 256 + (data[13] & 0xFF));
wakeupCount = ((data[16] & 0xFF) * 256 + (data[15] & 0xFF));
int hour = data[17] & 0xFF;
int minute = data[18] & 0xFF;
Calendar sleepStart = GregorianCalendar.getInstance();
sleepStart.clear();
sleepStart.set(Calendar.YEAR, year);
sleepStart.set(Calendar.MONTH, month - 1);
sleepStart.set(Calendar.DAY_OF_MONTH, day);
sleepStart.set(Calendar.HOUR_OF_DAY, hour);
sleepStart.set(Calendar.MINUTE, minute);
sleepStart.set(Calendar.SECOND, 0);
sleepStart.set(Calendar.MILLISECOND, 0);
bedTimeStart = (int) (sleepStart.getTimeInMillis() / 1000);
bedTimeEnd = (enterSleepMinutes + spindleMinutes + deepSleepMinutes + remSleepMinutes + wakeupMinutes) * 60 + bedTimeStart;
lightSleepMinutes = enterSleepMinutes + spindleMinutes + remSleepMinutes;
timestamp = bedTimeStart;
}
public List<RecordInterval> getIntervals() {
List<RecordInterval> intervals = new ArrayList<>();
int ts = bedTimeStart + lightSleepMinutes * 60;
intervals.add(new RecordInterval(bedTimeStart, ts, ActivityKind.TYPE_LIGHT_SLEEP));
intervals.add(new RecordInterval(ts, bedTimeEnd, ActivityKind.TYPE_DEEP_SLEEP));
return intervals;
}
public String toString(){
Calendar s = GregorianCalendar.getInstance();
s.setTimeInMillis(bedTimeStart * 1000L);
Calendar end = GregorianCalendar.getInstance();
end.setTimeInMillis(bedTimeEnd * 1000L);
return String.format(Locale.US, "Sleep start: %s end: %s enter: %d spindles: %d rem: %d deep: %d wake: %d-%d", s.getTime(), end.getTime(), enterSleepMinutes, spindleMinutes, remSleepMinutes, deepSleepMinutes, wakeupMinutes, wakeupCount);
}
}

View File

@ -0,0 +1,547 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import android.content.Context;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
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.devices.hplus.HPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusHealthSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlay;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlayDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
class HPlusHandlerThread extends GBDeviceIoThread {
private static final Logger LOG = LoggerFactory.getLogger(HPlusHandlerThread.class);
private int CURRENT_DAY_SYNC_PERIOD = 24 * 60 * 60 * 365; //Never
private int CURRENT_DAY_SYNC_RETRY_PERIOD = 10;
private int SLEEP_SYNC_PERIOD = 12 * 60 * 60;
private int SLEEP_SYNC_RETRY_PERIOD = 30;
private int DAY_SUMMARY_SYNC_PERIOD = 24 * 60 * 60;
private int DAY_SUMMARY_SYNC_RETRY_PERIOD = 30;
private boolean mQuit = false;
private HPlusSupport mHPlusSupport;
private int mLastSlotReceived = -1;
private int mLastSlotRequested = 0;
private Calendar mLastSleepDayReceived = GregorianCalendar.getInstance();
private Calendar mGetDaySlotsTime = GregorianCalendar.getInstance();
private Calendar mGetSleepTime = GregorianCalendar.getInstance();
private Calendar mGetDaySummaryTime = GregorianCalendar.getInstance();
private boolean mSlotsInitialSync = true;
private HPlusDataRecordRealtime prevRealTimeRecord = null;
private final Object waitObject = new Object();
List<HPlusDataRecordDaySlot> mDaySlotRecords = new ArrayList<>();
private HPlusDataRecordDaySlot mCurrentDaySlot = null;
public HPlusHandlerThread(GBDevice gbDevice, Context context, HPlusSupport hplusSupport) {
super(gbDevice, context);
mQuit = false;
mHPlusSupport = hplusSupport;
}
@Override
public void run() {
mQuit = false;
sync();
long waitTime = 0;
while (!mQuit) {
if (waitTime > 0) {
synchronized (waitObject) {
try {
waitObject.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (mQuit) {
break;
}
if(!mHPlusSupport.getDevice().isConnected()){
quit();
break;
}
Calendar now = GregorianCalendar.getInstance();
if (now.compareTo(mGetDaySlotsTime) > 0) {
requestNextDaySlots();
}
if (now.compareTo(mGetSleepTime) > 0) {
requestNextSleepData();
}
if(now.compareTo(mGetDaySummaryTime) > 0) {
requestDaySummaryData();
}
now = GregorianCalendar.getInstance();
waitTime = Math.min(mGetDaySummaryTime.getTimeInMillis(), Math.min(mGetDaySlotsTime.getTimeInMillis(), mGetSleepTime.getTimeInMillis())) - now.getTimeInMillis();
}
}
@Override
public void quit() {
mQuit = true;
synchronized (waitObject) {
waitObject.notify();
}
}
public void sync() {
mGetSleepTime.setTimeInMillis(0);
mGetDaySlotsTime.setTimeInMillis(0);
mGetDaySummaryTime.setTimeInMillis(0);
mLastSleepDayReceived.setTimeInMillis(0);
mSlotsInitialSync = true;
mLastSlotReceived = -1;
mLastSlotRequested = 0;
mCurrentDaySlot = null;
mDaySlotRecords.clear();
TransactionBuilder builder = new TransactionBuilder("startSyncDayStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA});
builder.queue(mHPlusSupport.getQueue());
synchronized (waitObject) {
waitObject.notify();
}
}
/**
* Process a message containing information regarding a day slot
* A slot summarizes 10 minutes of data
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processIncomingDaySlotData(byte[] data) {
HPlusDataRecordDaySlot record;
try{
record = new HPlusDataRecordDaySlot(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
Calendar now = GregorianCalendar.getInstance();
int nowSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + (now.get(Calendar.MINUTE) / 10);
if(record.slot == nowSlot){
if(mCurrentDaySlot != null && mCurrentDaySlot != record){
mCurrentDaySlot.accumulate(record);
mDaySlotRecords.add(mCurrentDaySlot);
mCurrentDaySlot = null;
}else{
//Store it to a temp variable as this is an intermediate value
mCurrentDaySlot = record;
if(!mSlotsInitialSync)
return true;
}
}
if(mSlotsInitialSync) {
//If the slot is in the future, actually it is from the previous day
//Subtract a day of seconds
if(record.slot > nowSlot){
record.timestamp -= 3600 * 24;
}
if (record.slot == mLastSlotReceived + 1) {
mLastSlotReceived = record.slot;
}
//Ignore the current slot as it is incomplete
if(record.slot != nowSlot)
mDaySlotRecords.add(record);
//Still fetching ring buffer. Request the next slots
if (record.slot == mLastSlotRequested) {
mGetDaySlotsTime.clear();
synchronized (waitObject) {
waitObject.notify();
}
}
//Keep buffering
if(record.slot != 143)
return true;
} else {
mGetDaySlotsTime = GregorianCalendar.getInstance();
mGetDaySlotsTime.add(Calendar.DAY_OF_MONTH, 1);
}
if(mDaySlotRecords.size() > 0) {
//Sort the samples
Collections.sort(mDaySlotRecords, new Comparator<HPlusDataRecordDaySlot>() {
public int compare(HPlusDataRecordDaySlot one, HPlusDataRecordDaySlot other) {
return one.timestamp - other.timestamp;
}
});
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
List<HPlusHealthActivitySample> samples = new ArrayList<>();
for (HPlusDataRecordDaySlot storedRecord : mDaySlotRecords) {
HPlusHealthActivitySample sample = createSample(dbHandler, storedRecord.timestamp);
sample.setRawHPlusHealthData(storedRecord.getRawData());
sample.setSteps(storedRecord.steps);
sample.setHeartRate(storedRecord.heartRate);
sample.setRawKind(storedRecord.type);
sample.setProvider(provider);
samples.add(sample);
}
provider.getSampleDao().insertOrReplaceInTx(samples);
mDaySlotRecords.clear();
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
}
return true;
}
/**
* Process sleep data from the device
* Devices send a single sleep message for each sleep period
* This message contains the duration of the sub-intervals (rem, deep, etc...)
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processIncomingSleepData(byte[] data){
HPlusDataRecordSleep record;
try{
record = new HPlusDataRecordSleep(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
mLastSleepDayReceived.setTimeInMillis(record.bedTimeStart * 1000L);
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Long userId = DBHelper.getUser(session).getId();
Long deviceId = DBHelper.getDevice(getDevice(), session).getId();
HPlusHealthActivityOverlayDao overlayDao = session.getHPlusHealthActivityOverlayDao();
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
//Get the individual Sleep overlays and insert them
List<HPlusHealthActivityOverlay> overlayList = new ArrayList<>();
List<HPlusDataRecord.RecordInterval> intervals = record.getIntervals();
for(HPlusDataRecord.RecordInterval interval : intervals){
overlayList.add(new HPlusHealthActivityOverlay(interval.timestampFrom, interval.timestampTo, interval.activityKind, deviceId, userId, null));
}
overlayDao.insertOrReplaceInTx(overlayList);
//Store the data
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
sample.setRawHPlusHealthData(record.getRawData());
sample.setRawKind(record.activityKind);
sample.setProvider(provider);
provider.addGBActivitySample(sample);
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
mGetSleepTime = GregorianCalendar.getInstance();
mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_PERIOD);
return true;
}
/**
* Process a message containing real time information
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processRealtimeStats(byte[] data) {
HPlusDataRecordRealtime record;
try{
record = new HPlusDataRecordRealtime(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
//Skip duplicated messages as the device seems to send the same record multiple times
//This can be used to detect the user is moving (not sleeping)
if(prevRealTimeRecord != null && record.same(prevRealTimeRecord))
return true;
prevRealTimeRecord = record;
getDevice().setBatteryLevel(record.battery);
//Skip when measuring heart rate
//Calories and Distance are updated and these values will be lost.
//Because a message with a valid Heart Rate will be provided, this loss very limited
if(record.heartRate == ActivityKind.TYPE_NOT_MEASURED) {
getDevice().setFirmwareVersion2("---");
getDevice().sendDeviceUpdateIntent(getContext());
}else {
getDevice().setFirmwareVersion2("" + record.heartRate);
getDevice().sendDeviceUpdateIntent(getContext());
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
sample.setRawKind(record.type);
sample.setRawIntensity(record.intensity);
sample.setHeartRate(record.heartRate);
sample.setDistance(record.distance);
sample.setCalories(record.calories);
sample.setSteps(record.steps);
sample.setRawHPlusHealthData(record.getRawData());
sample.setProvider(provider);
provider.addGBActivitySample(sample);
sample.setSteps(sample.getSteps() - prevRealTimeRecord.steps);
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample)
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
//TODO: Handle Active Time. With Overlay?
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
return true;
}
/**
* Process a day summary message
* This message includes aggregates regarding an entire day
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processDaySummary(byte[] data) {
HPlusDataRecordDaySummary record;
try{
record = new HPlusDataRecordDaySummary(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
sample.setRawKind(record.type);
sample.setSteps(record.steps);
sample.setDistance(record.distance);
sample.setCalories(record.calories);
sample.setDistance(record.distance);
sample.setHeartRate((record.maxHeartRate - record.minHeartRate) / 2); //TODO: Find an alternative approach for Day Summary Heart Rate
sample.setRawHPlusHealthData(record.getRawData());
sample.setProvider(provider);
provider.addGBActivitySample(sample);
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
mGetDaySummaryTime = GregorianCalendar.getInstance();
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_PERIOD);
return true;
}
/**
* Process a message containing information regarding firmware version
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processVersion(byte[] data) {
int major = data[2] & 0xFF;
int minor = data[1] & 0xFF;
getDevice().setFirmwareVersion(major + "." + minor);
getDevice().sendDeviceUpdateIntent(getContext());
return true;
}
/**
* Issue a message requesting the next batch of sleep data
*/
private void requestNextSleepData() {
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
builder.queue(mHPlusSupport.getQueue());
mGetSleepTime = GregorianCalendar.getInstance();
mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_RETRY_PERIOD);
}
/**
* Issue a message requesting the next set of slots
* The process will sync 1h at a time until the device is in sync
* Then it will request samples until the end of the day in order to minimize data loss
* Messages will be provided every 10 minutes after they are available
*/
private void requestNextDaySlots() {
Calendar now = GregorianCalendar.getInstance();
int currentSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + now.get(Calendar.MINUTE) / 10;
//Finished dumping the entire ring buffer
//Sync to current time
mGetDaySlotsTime = now;
if(mSlotsInitialSync) {
if(mLastSlotReceived == 143) {
mSlotsInitialSync = false;
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); //Sync complete. Delay timer forever
mLastSlotReceived = -1;
mLastSlotRequested = mLastSlotReceived + 1;
return;
}else {
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_RETRY_PERIOD);
}
}else{
//Sync complete. Delay timer forever
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
return;
}
if(mLastSlotReceived == 143)
mLastSlotReceived = -1;
byte hour = (byte) ((mLastSlotReceived + 1)/ 6);
byte minute = (byte) (((mLastSlotReceived + 1) % 6) * 10);
byte nextHour = hour;
byte nextMinute = 59;
mLastSlotRequested = nextHour * 6 + (nextMinute / 10);
byte[] msg = new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY, hour, minute, nextHour, nextMinute};
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
builder.queue(mHPlusSupport.getQueue());
}
/**
* Request a batch of data with the summary of the previous days
*/
public void requestDaySummaryData(){
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
builder.queue(mHPlusSupport.getQueue());
mGetDaySummaryTime = GregorianCalendar.getInstance();
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_RETRY_PERIOD);
}
/**
* Helper function to create a sample
* @param dbHandler The database handler
* @param timestamp The sample timestamp
* @return The sample just created
*/
private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp){
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
timestamp, // ts
deviceId, userId, // User id
null, // Raw Data
ActivityKind.TYPE_UNKNOWN,
0, // Intensity
ActivitySample.NOT_MEASURED, // Steps
ActivitySample.NOT_MEASURED, // HR
ActivitySample.NOT_MEASURED, // Distance
ActivitySample.NOT_MEASURED // Calories
);
return sample;
}
}

View File

@ -0,0 +1,828 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
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.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
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;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class HPlusSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(HPlusSupport.class);
public BluetoothGattCharacteristic ctrlCharacteristic = null;
public BluetoothGattCharacteristic measureCharacteristic = null;
private HPlusHandlerThread syncHelper;
private DeviceType deviceType = DeviceType.UNKNOWN;
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));
}
}
};
public HPlusSupport(DeviceType type) {
super(LOG);
deviceType = type;
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(HPlusConstants.UUID_SERVICE_HP);
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
IntentFilter intentFilter = new IntentFilter();
broadcastManager.registerReceiver(mReceiver, intentFilter);
}
@Override
public void dispose() {
LOG.debug("Dispose");
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
broadcastManager.unregisterReceiver(mReceiver);
close();
super.dispose();
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
LOG.debug("Initializing");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
measureCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE);
ctrlCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_CONTROL);
getDevice().setFirmwareVersion("N/A");
getDevice().setFirmwareVersion2("N/A");
syncHelper = new HPlusHandlerThread(getDevice(), getContext(), this);
//Initialize device
sendUserInfo(builder); //Sync preferences
requestDeviceInfo(builder);
setInitialized(builder);
syncHelper.start();
builder.notify(getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE), true);
builder.setGattCallback(this);
builder.notify(measureCharacteristic, true);
return builder;
}
private HPlusSupport sendUserInfo(TransactionBuilder builder) {
builder.write(ctrlCharacteristic, HPlusConstants.CMD_SET_PREF_START);
builder.write(ctrlCharacteristic, HPlusConstants.CMD_SET_PREF_START1);
syncPreferences(builder);
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_CONF_END});
return this;
}
private HPlusSupport syncPreferences(TransactionBuilder transaction) {
if(deviceType == DeviceType.HPLUS) {
setSIT(transaction); //Sync SIT Interval
}
setCurrentDate(transaction);
setCurrentTime(transaction);
setDayOfWeek(transaction);
setTimeMode(transaction);
setGender(transaction);
setAge(transaction);
setWeight(transaction);
setHeight(transaction);
setGoal(transaction);
setLanguage(transaction);
setScreenTime(transaction);
setUnit(transaction);
setAllDayHeart(transaction);
return this;
}
private HPlusSupport setLanguage(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getLanguage(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_LANGUAGE,
value
});
return this;
}
private HPlusSupport setTimeMode(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getTimeMode(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_TIMEMODE,
value
});
return this;
}
private HPlusSupport setUnit(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUnit(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_UNITS,
value
});
return this;
}
private HPlusSupport setCurrentDate(TransactionBuilder transaction) {
Calendar c = GregorianCalendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int day = c.get(Calendar.DAY_OF_MONTH);
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_DATE,
(byte) ((year / 256) & 0xff),
(byte) (year % 256),
(byte) (month + 1),
(byte) (day)
});
return this;
}
private HPlusSupport setCurrentTime(TransactionBuilder transaction) {
Calendar c = GregorianCalendar.getInstance();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_TIME,
(byte) c.get(Calendar.HOUR_OF_DAY),
(byte) c.get(Calendar.MINUTE),
(byte) c.get(Calendar.SECOND)
});
return this;
}
private HPlusSupport setDayOfWeek(TransactionBuilder transaction) {
Calendar c = GregorianCalendar.getInstance();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_WEEK,
(byte) (c.get(Calendar.DAY_OF_WEEK) - 1)
});
return this;
}
private HPlusSupport setSIT(TransactionBuilder transaction) {
//Makibes F68 doesn't like this command.
//Just ignore.
if(deviceType == DeviceType.MAKIBESF68){
return this;
}
int startTime = HPlusCoordinator.getSITStartTime(getDevice().getAddress());
int endTime = HPlusCoordinator.getSITEndTime(getDevice().getAddress());
Calendar now = GregorianCalendar.getInstance();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_SIT_INTERVAL,
(byte) ((startTime / 256) & 0xff),
(byte) (startTime % 256),
(byte) ((endTime / 256) & 0xff),
(byte) (endTime % 256),
0,
0,
(byte) ((now.get(Calendar.YEAR) / 256) & 0xff),
(byte) (now.get(Calendar.YEAR) % 256),
(byte) (now.get(Calendar.MONTH) + 1),
(byte) (now.get(Calendar.DAY_OF_MONTH)),
(byte) (now.get(Calendar.HOUR_OF_DAY)),
(byte) (now.get(Calendar.MINUTE)),
(byte) (now.get(Calendar.SECOND)),
0,
0,
0,
0
});
return this;
}
private HPlusSupport setWeight(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserWeight(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_WEIGHT,
value
});
return this;
}
private HPlusSupport setHeight(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserHeight(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_HEIGHT,
value
});
return this;
}
private HPlusSupport setAge(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserAge(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_AGE,
value
});
return this;
}
private HPlusSupport setGender(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserGender(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_GENDER,
value
});
return this;
}
private HPlusSupport setGoal(TransactionBuilder transaction) {
int value = HPlusCoordinator.getGoal(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_GOAL,
(byte) ((value / 256) & 0xff),
(byte) (value % 256)
});
return this;
}
private HPlusSupport setScreenTime(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getScreenTime(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_SCREENTIME,
value
});
return this;
}
private HPlusSupport setAllDayHeart(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getAllDayHR(getDevice().getAddress());
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_ALLDAY_HRM,
value
});
return this;
}
private HPlusSupport setAlarm(TransactionBuilder transaction, Calendar t) {
byte hour = HPlusConstants.ARG_ALARM_DISABLE;
byte minute = HPlusConstants.ARG_ALARM_DISABLE;
if(t != null){
hour = (byte) t.get(Calendar.HOUR_OF_DAY);
minute = (byte) t.get(Calendar.MINUTE);
}
transaction.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALARM,
hour,
minute});
return this;
}
private HPlusSupport setFindMe(TransactionBuilder transaction, boolean state) {
//TODO: Find how this works
byte[] msg = new byte[2];
msg[0] = HPlusConstants.CMD_SET_FINDME;
if (state)
msg[1] = HPlusConstants.ARG_FINDME_ON;
else
msg[1] = HPlusConstants.ARG_FINDME_OFF;
transaction.write(ctrlCharacteristic, msg);
return this;
}
private HPlusSupport requestDeviceInfo(TransactionBuilder builder) {
// HPlus devices seem to report some information in an alternative manner
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID});
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION});
return this;
}
private void setInitialized(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
}
@Override
public boolean useAutoConnect() {
return true;
}
@Override
public void pair() {
LOG.debug("Pair");
}
private void handleDeviceInfo(DeviceInfo info) {
LOG.warn("Device info: " + info);
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
//TODO: Show different notifications according to source as Band supports this
//LOG.debug("OnNotification: Title: "+notificationSpec.title+" Body: "+notificationSpec.body+" Source: "+notificationSpec.sourceName+" Sender: "+notificationSpec.sender+" Subject: "+notificationSpec.subject);
showText(notificationSpec.title, notificationSpec.body);
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
TransactionBuilder builder = new TransactionBuilder("time");
setCurrentDate(builder);
setCurrentTime(builder);
builder.queue(getQueue());
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
TransactionBuilder builder = new TransactionBuilder("alarm");
for (Alarm alarm : alarms) {
if (!alarm.isEnabled())
continue;
if (alarm.isSmartWakeup()) //Not available
continue;
Calendar t = alarm.getAlarmCal();
setAlarm(builder, t);
builder.queue(getQueue());
GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO);
return; //Only first alarm
}
setAlarm(builder, null);
builder.queue(getQueue());
GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO);
}
@Override
public void onSetCallState(CallSpec callSpec) {
switch (callSpec.command) {
case CallSpec.CALL_INCOMING: {
showIncomingCall(callSpec.name, callSpec.number);
break;
}
}
}
@Override
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
LOG.debug("Canned Messages: " + cannedMessagesSpec);
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
}
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
}
@Override
public void onEnableRealtimeSteps(boolean enable) {
onEnableRealtimeHeartRateMeasurement(enable);
}
@Override
public void onInstallApp(Uri uri) {
}
@Override
public void onAppInfoReq() {
}
@Override
public void onAppStart(UUID uuid, boolean start) {
}
@Override
public void onAppDelete(UUID uuid) {
}
@Override
public void onAppConfiguration(UUID appUuid, String config) {
}
@Override
public void onAppReorder(UUID[] uuids) {
}
@Override
public void onFetchActivityData() {
if (syncHelper != null)
syncHelper.sync();
}
@Override
public void onReboot() {
getQueue().clear();
TransactionBuilder builder = new TransactionBuilder("Shutdown");
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SHUTDOWN, HPlusConstants.ARG_SHUTDOWN_EN});
builder.queue(getQueue());
}
@Override
public void onHeartRateTest() {
getQueue().clear();
TransactionBuilder builder = new TransactionBuilder("HeartRateTest");
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_HEARTRATE_STATE, HPlusConstants.ARG_HEARTRATE_MEASURE_ON}); //Set Real Time... ?
builder.queue(getQueue());
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
getQueue().clear();
TransactionBuilder builder = new TransactionBuilder("realTimeHeartMeasurement");
byte state;
if (enable)
state = HPlusConstants.ARG_HEARTRATE_ALLDAY_ON;
else
state = HPlusConstants.ARG_HEARTRATE_ALLDAY_OFF;
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALLDAY_HRM, state});
builder.queue(getQueue());
}
@Override
public void onFindDevice(boolean start) {
try {
TransactionBuilder builder = performInitialized("findMe");
setFindMe(builder, start);
builder.queue(getQueue());
} catch (IOException e) {
GB.toast(getContext(), "Error toggling Find Me: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
@Override
public void onSetConstantVibration(int intensity) {
getQueue().clear();
try {
TransactionBuilder builder = performInitialized("vibration");
byte[] msg = new byte[15];
msg[0] = HPlusConstants.CMD_SET_INCOMING_CALL_NUMBER;
for (int i = 0; i < msg.length - 1; i++)
msg[i + 1] = (byte) "GadgetBridge".charAt(i);
builder.write(ctrlCharacteristic, msg);
builder.queue(getQueue());
} catch (IOException e) {
GB.toast(getContext(), "Error setting Vibration: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
@Override
public void onScreenshotReq() {
}
@Override
public void onEnableHeartRateSleepSupport(boolean enable) {
onEnableRealtimeHeartRateMeasurement(enable);
}
@Override
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
}
@Override
public void onDeleteCalendarEvent(byte type, long id) {
}
@Override
public void onSendConfiguration(String config) {
LOG.debug("Send Configuration: " + config);
}
@Override
public void onTestNewFunction() {
LOG.debug("Test New Function");
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
}
private void showIncomingCall(String name, String rawNumber) {
try {
StringBuilder number = new StringBuilder();
//Clean up number as the device only accepts digits
for(char c : rawNumber.toCharArray()){
if(Character.isDigit(c)){
number.append(c);
}
}
TransactionBuilder builder = performInitialized("incomingCall");
//Enable call notifications
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_ACTION_INCOMING_CALL, 1});
//Show Call Icon
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_CALL, HPlusConstants.ARG_INCOMING_CALL});
byte[] msg = new byte[13];
//Show call number
for (int i = 0; i < msg.length; i++)
msg[i] = ' ';
for (int i = 0; i < number.length() && i < (msg.length - 1); i++)
msg[i + 1] = (byte) number.charAt(i);
msg[0] = HPlusConstants.CMD_SET_INCOMING_CALL_NUMBER;
builder.write(ctrlCharacteristic, msg);
builder.wait(200);
msg = msg.clone();
//Show call name
for (int i = 0; i < msg.length; i++)
msg[i] = ' ';
byte[] nameBytes = encodeStringToDevice(name);
for (int i = 0; i < nameBytes.length && i < (msg.length - 1); i++)
msg[i + 1] = nameBytes[i];
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME;
builder.write(ctrlCharacteristic, msg);
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME_CN;
builder.write(ctrlCharacteristic, msg);
builder.queue(getQueue());
} catch (IOException e) {
GB.toast(getContext(), "Error showing incoming call: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
private void showText(String title, String body) {
LOG.debug("Show Notification: "+title+" --> "+body);
try {
TransactionBuilder builder = performInitialized("notification");
String message = "";
if (title != null && title.length() > 0) {
message = StringUtils.pad(StringUtils.truncate(title, 16), 16); //Limit title to top row
}
if(body != null) {
message += body;
}
byte[] messageBytes = encodeStringToDevice(message);
int length = messageBytes.length / 17;
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_MESSAGE, HPlusConstants.ARG_INCOMING_MESSAGE});
int remaining = Math.min(255, (messageBytes.length % 17 > 0) ? length + 1 : length);
byte[] msg = new byte[20];
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT;
msg[1] = (byte) remaining;
for (int i = 2; i < msg.length; i++)
msg[i] = ' ';
int message_index = 0;
int i = 3;
for (int j = 0; j < messageBytes.length; j++) {
msg[i++] = messageBytes[j];
if (i == msg.length) {
message_index++;
msg[2] = (byte) message_index;
builder.write(ctrlCharacteristic, msg);
msg = msg.clone();
for (i = 3; i < msg.length; i++)
msg[i] = ' ';
if (message_index < remaining)
i = 3;
else
break;
}
}
msg[2] = (byte) remaining;
builder.write(ctrlCharacteristic, msg);
builder.queue(getQueue());
} catch (IOException e) {
GB.toast(getContext(), "Error showing device Notification: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
private void close() {
if (syncHelper != null) {
syncHelper.quit();
syncHelper = null;
}
}
/**
* HPlus devices accept a subset of GB2312 with some modifications.
* This function will apply a custom transliteration.
* While related to the methods implemented in LanguageUtils. These are specific for HPLUS
*
* @param s The String to transliterate
* @return An array of bytes ready to be sent to the device
*/
private byte[] encodeStringToDevice(String s){
List<Byte> outBytes = new ArrayList<Byte>();
for(int i = 0; i < s.length(); i++){
Character c = s.charAt(i);
byte[] cs;
if(HPlusConstants.transliterateMap.containsKey(c)){
cs = new byte[] {HPlusConstants.transliterateMap.get(c)};
}else {
try {
cs = c.toString().getBytes("GB2312");
} catch (UnsupportedEncodingException e) {
//Fallback. Result string may be strange, but better than nothing
cs = c.toString().getBytes();
}
}
for(int j = 0; j < cs.length; j++)
outBytes.add(cs[j]);
}
return ArrayUtils.toPrimitive(outBytes.toArray(new Byte[outBytes.size()]));
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
if (super.onCharacteristicChanged(gatt, characteristic)) {
return true;
}
UUID characteristicUUID = characteristic.getUuid();
byte[] data = characteristic.getValue();
if (data.length == 0)
return true;
switch (data[0]) {
case HPlusConstants.DATA_VERSION:
return syncHelper.processVersion(data);
case HPlusConstants.DATA_STATS:
return syncHelper.processRealtimeStats(data);
case HPlusConstants.DATA_SLEEP:
return syncHelper.processIncomingSleepData(data);
case HPlusConstants.DATA_STEPS:
return syncHelper.processDaySummary(data);
case HPlusConstants.DATA_DAY_SUMMARY:
case HPlusConstants.DATA_DAY_SUMMARY_ALT:
return syncHelper.processIncomingDaySlotData(data);
default:
LOG.debug("Unhandled characteristic changed: " + characteristicUUID);
return true;
}
}
}

View File

@ -50,6 +50,8 @@ 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.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
@ -101,6 +103,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
private RealtimeSamplesSupport realtimeSamplesSupport;
private boolean alarmClockRining;
private boolean alarmClockRinging;
public MiBandSupport() {
super(LOG);
@ -540,10 +544,31 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) {
onAlarmClock(notificationSpec);
return;
}
String origin = notificationSpec.type.getGenericType();
performPreferredNotification(origin + " received", origin, null);
}
private void onAlarmClock(NotificationSpec notificationSpec) {
alarmClockRining = true;
AbortTransactionAction abortAction = new AbortTransactionAction() {
@Override
protected boolean shouldAbort() {
return !isAlarmClockRinging();
}
};
performPreferredNotification("alarm clock ringing", MiBandConst.ORIGIN_ALARM_CLOCK, abortAction);
}
@Override
public void onDeleteNotification(int id) {
alarmClockRining = false; // we should have the notificationtype at least to check
}
@Override
public void onSetTime() {
try {
@ -610,6 +635,10 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
}
private boolean isAlarmClockRinging() {
// don't synchronize, this is not really important
return alarmClockRinging;
}
private boolean isTelephoneRinging() {
// don't synchronize, this is not really important
return telephoneRinging;
@ -950,30 +979,25 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
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);
// set the steps only afterwards, since realtime steps are also recorded
// in the regular samples and we must not count them twice
// Note: we know that the DAO sample is never committed again, so we simply
// change the value here in memory.
sample.setSteps(getSteps());
if (LOG.isDebugEnabled()) {
LOG.debug("realtime sample: " + sample);
}
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
} catch (Exception e) {
LOG.warn("Unable to acquire db for saving realtime samples", e);
}
@ -1211,6 +1235,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
}
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
}
private void handleSensorData(byte[] value) {
int counter=0, step=0, axis1=0, axis2=0, axis3 =0;
if ((value.length - 2) % 6 != 0) {

View File

@ -22,6 +22,7 @@ public abstract class RealtimeSamplesSupport {
protected int steps;
protected int heartrateBpm;
private int lastSteps;
// subclasses may add more
private Timer realtimeStorageTimer;
@ -56,12 +57,27 @@ public abstract class RealtimeSamplesSupport {
return realtimeStorageTimer != null;
}
public void setSteps(int stepsPerMinute) {
public synchronized void setSteps(int stepsPerMinute) {
this.steps = stepsPerMinute;
}
public int getSteps() {
return steps;
/**
* Returns the number of steps recorded since the last measurements. If no
* steps are available yet, ActivitySample.NOT_MEASURED is returned.
* @return
*/
public synchronized int getSteps() {
if (steps == ActivitySample.NOT_MEASURED) {
return ActivitySample.NOT_MEASURED;
}
if (lastSteps == 0) {
return ActivitySample.NOT_MEASURED; // wait until we have a delta between two samples
}
int delta = steps - lastSteps;
if (delta < 0) {
return 0;
}
return delta;
}
public void setHeartrateBpm(int hrBpm) {
@ -77,7 +93,10 @@ public abstract class RealtimeSamplesSupport {
resetCurrentValues();
}
protected void resetCurrentValues() {
protected synchronized void resetCurrentValues() {
if (steps >= lastSteps) {
lastSteps = steps;
}
steps = ActivitySample.NOT_MEASURED;
heartrateBpm = ActivitySample.NOT_MEASURED;
}

View File

@ -55,6 +55,7 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
private final boolean hasExtendedActivityData;
private static class ActivityStruct {
private int lastNotifiedProgress;
private final byte[] activityDataHolder;
private final int activityDataHolderSize;
//index of the buffer above
@ -129,6 +130,7 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
public void bufferFlushed(int minutes) {
activityDataTimestampProgress.add(Calendar.MINUTE, minutes);
activityDataHolderProgress = 0;
lastNotifiedProgress = 0;
}
}
@ -199,9 +201,16 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
} else {
bufferActivityData(value);
}
LOG.debug("activity data: length: " + value.length + ", remaining bytes: " + activityStruct.activityDataRemainingBytes);
if (LOG.isDebugEnabled()) {
LOG.debug("activity data: length: " + value.length + ", remaining bytes: " + activityStruct.activityDataRemainingBytes);
}
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), true, (int) (((float) (activityStruct.activityDataUntilNextHeader - activityStruct.activityDataRemainingBytes)) / activityStruct.activityDataUntilNextHeader * 100), getContext());
int progress = (int) (((float) (activityStruct.activityDataUntilNextHeader - activityStruct.activityDataRemainingBytes)) / activityStruct.activityDataUntilNextHeader * 100);
// avoid too many notifications overloading the system
if (progress - activityStruct.lastNotifiedProgress >= 8) {
activityStruct.lastNotifiedProgress = progress;
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), true, progress, getContext());
}
if (activityStruct.isBlockFinished()) {
sendAckDataTransfer(activityStruct.activityDataTimestampToAck, activityStruct.activityDataUntilNextHeader);

View File

@ -57,6 +57,7 @@ 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.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
@ -70,9 +71,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.Dev
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.CheckAuthenticationNeededAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.actions.StopNotificationAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation;
@ -123,6 +124,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
private RealtimeSamplesSupport realtimeSamplesSupport;
private boolean alarmClockRinging;
public MiBand2Support() {
super(LOG);
@ -271,6 +273,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), enable);
builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO), enable);
builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_10_BUTTON), enable);
BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT);
if (heartrateCharacteristic != null) {
builder.notify(heartrateCharacteristic, enable);
@ -586,6 +589,10 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) {
onAlarmClock(notificationSpec);
return;
}
int alertLevel = MiBand2Service.ALERT_LEVEL_MESSAGE;
if (notificationSpec.type == NotificationType.UNKNOWN) {
alertLevel = MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY;
@ -594,6 +601,22 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
performPreferredNotification(origin + " received", origin, alertLevel, null);
}
private void onAlarmClock(NotificationSpec notificationSpec) {
alarmClockRinging = true;
AbortTransactionAction abortAction = new StopNotificationAction(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL)) {
@Override
protected boolean shouldAbort() {
return !isAlarmClockRinging();
}
};
performPreferredNotification("alarm clock ringing", MiBandConst.ORIGIN_ALARM_CLOCK, MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY, abortAction);
}
@Override
public void onDeleteNotification(int id) {
alarmClockRinging = false; // we should have the notificationtype at least to check
}
@Override
public void onSetTime() {
try {
@ -611,23 +634,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
public void onSetCallState(CallSpec callSpec) {
if (callSpec.command == CallSpec.CALL_INCOMING) {
telephoneRinging = true;
AbortTransactionAction abortAction = new AbortTransactionAction() {
AbortTransactionAction abortAction = new StopNotificationAction(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL)) {
@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)) {
@ -639,6 +650,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
}
private boolean isAlarmClockRinging() {
// don't synchronize, this is not really important
return alarmClockRinging;
}
private boolean isTelephoneRinging() {
// don't synchronize, this is not really important
return telephoneRinging;
@ -843,6 +859,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
LOG.info("AUTHENTICATION?? " + characteristicUUID);
logMessageContent(characteristic.getValue());
return true;
} else if (MiBand2Service.UUID_CHARACTERISTIC_10_BUTTON.equals(characteristicUUID)) {
handleButtonPressed(characteristic.getValue());
return true;
} else {
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
logMessageContent(characteristic.getValue());
@ -850,6 +869,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return false;
}
private void handleButtonPressed(byte[] value) {
LOG.info("Button pressed: " + value);
logMessageContent(value);
}
private void handleUnknownCharacteristic(byte[] value) {
}
@ -872,6 +896,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
} else if (MiBandService.UUID_CHARACTERISTIC_DATE_TIME.equals(characteristicUUID)) {
logDate(characteristic.getValue(), status);
return true;
} else if (MiBand2Service.UUID_CHARACTERISTIC_10_BUTTON.equals(characteristicUUID)) {
handleButtonPressed(characteristic.getValue());
return true;
} else {
LOG.info("Unhandled characteristic read: " + characteristicUUID);
logMessageContent(characteristic.getValue());
@ -983,26 +1010,22 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
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);
// set the steps only afterwards, since realtime steps are also recorded
// in the regular samples and we must not count them twice
// Note: we know that the DAO sample is never committed again, so we simply
// change the value here in memory.
sample.setSteps(getSteps());
if (LOG.isDebugEnabled()) {
LOG.debug("realtime sample: " + sample);
}
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
} catch (Exception e) {
LOG.warn("Unable to acquire db for saving realtime samples", e);
}
@ -1249,6 +1272,18 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
@Override
public void onTestNewFunction() {
try {
performInitialized("read characteristic 10")
.read(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_10_BUTTON))
.queue(getQueue());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
}
private MiBand2Support setDateDisplay(TransactionBuilder builder) {

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.actions;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction;
public abstract class StopNotificationAction extends AbortTransactionAction {
private final BluetoothGattCharacteristic alertLevelCharacteristic;
public StopNotificationAction(BluetoothGattCharacteristic alertLevelCharacteristic) {
this.alertLevelCharacteristic = alertLevelCharacteristic;
}
@Override
public boolean run(BluetoothGatt gatt) {
if (!super.run(gatt)) {
// send a signal to stop the vibration
alertLevelCharacteristic.setValue(new byte[]{MiBand2Service.ALERT_LEVEL_NONE});
gatt.writeCharacteristic(alertLevelCharacteristic);
return false;
}
return true;
}
};

View File

@ -2,6 +2,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.widget.Toast;
import org.slf4j.Logger;
@ -23,7 +25,6 @@ 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;
@ -73,7 +74,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
builder.notify(characteristicFetch, true);
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime();
GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
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);
@ -81,26 +82,28 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
builder.queue(getQueue());
}
private GregorianCalendar getLastSuccessfulSynchronizedTime() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
SampleProvider<MiBandActivitySample> 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);
private GregorianCalendar getLastSuccessfulSyncTime() {
long timeStampMillis = GBApplication.getPrefs().getLong(getLastSyncTimeKey(), 0);
if (timeStampMillis != 0) {
GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.setTimeInMillis(timeStampMillis);
return calendar;
}
GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.add(Calendar.DAY_OF_MONTH, -10);
return calendar;
}
private void saveLastSyncTimestamp(@NonNull GregorianCalendar timestamp) {
SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit();
editor.putLong(getLastSyncTimeKey(), timestamp.getTimeInMillis());
editor.apply();
}
private String getLastSyncTimeKey() {
return getDevice().getAddress() + "_" + "lastSyncTimeMillis";
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
@ -147,6 +150,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
}
sampleProvider.addGBActivitySamples(samples.toArray(new MiBandActivitySample[0]));
saveLastSyncTimestamp(timestamp);
LOG.info("Mi2 activity data: last sample timestamp: " + DateTimeUtils.formatDateTime(timestamp.getTime()));
} catch (Exception ex) {

View File

@ -3,15 +3,25 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class AppMessageHandler {
protected final PebbleProtocol mPebbleProtocol;
protected final UUID mUUID;
class AppMessageHandler {
final PebbleProtocol mPebbleProtocol;
final UUID mUUID;
Map<String, Integer> messageKeys;
AppMessageHandler(UUID uuid, PebbleProtocol pebbleProtocol) {
mUUID = uuid;
@ -27,14 +37,32 @@ public class AppMessageHandler {
}
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
// Just ACK
GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes();
sendBytesAck.encodedBytes = mPebbleProtocol.encodeApplicationMessageAck(mUUID, mPebbleProtocol.last_id);
return new GBDeviceEvent[]{sendBytesAck};
}
public GBDeviceEvent[] onAppStart() {
return null;
}
public GBDeviceEvent[] pushMessage() {
public byte[] encodeUpdateWeather(WeatherSpec weatherSpec) {
return null;
}
protected GBDevice getDevice() {
return mPebbleProtocol.getDevice();
}
JSONObject getAppKeys() throws IOException, JSONException {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, mUUID.toString() + ".json");
if (configurationFile.exists()) {
String jsonstring = FileUtils.getStringFromFile(configurationFile);
JSONObject json = new JSONObject(jsonstring);
return json.getJSONObject("appKeys");
}
throw new IOException();
}
}

View File

@ -0,0 +1,69 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
class AppMessageHandlerHealthify extends AppMessageHandler {
private Integer KEY_TEMPERATURE;
private Integer KEY_CONDITIONS;
AppMessageHandlerHealthify(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
try {
JSONObject appKeys = getAppKeys();
KEY_TEMPERATURE = appKeys.getInt("TEMPERATURE");
KEY_CONDITIONS = appKeys.getInt("CONDITIONS");
} catch (JSONException e) {
GB.toast("There was an error accessing the Helthify watchface configuration.", Toast.LENGTH_LONG, GB.ERROR);
} catch (IOException ignore) {
}
}
private byte[] encodeHelthifyWeatherMessage(WeatherSpec weatherSpec) {
if (weatherSpec == null) {
return null;
}
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(KEY_CONDITIONS, (Object) weatherSpec.currentCondition));
pairs.add(new Pair<>(KEY_TEMPERATURE, (Object) (weatherSpec.currentTemp - 273)));
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
buf.put(weatherMessage);
return buf.array();
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
if (weatherSpec == null) {
return new GBDeviceEvent[]{null};
}
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodeHelthifyWeatherMessage(weatherSpec);
return new GBDeviceEvent[]{sendBytes};
}
@Override
public byte[] encodeUpdateWeather(WeatherSpec weatherSpec) {
return encodeHelthifyWeatherMessage(weatherSpec);
}
}

View File

@ -0,0 +1,55 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
class AppMessageHandlerMarioTime extends AppMessageHandler {
private static final int KEY_WEATHER_ICON_ID = 10;
private static final int KEY_WEATHER_TEMPERATURE = 11;
AppMessageHandlerMarioTime(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
}
private byte[] encodeMarioWeatherMessage(WeatherSpec weatherSpec) {
if (weatherSpec == null) {
return null;
}
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(KEY_WEATHER_ICON_ID, (Object) (byte) 1));
pairs.add(new Pair<>(KEY_WEATHER_TEMPERATURE, (Object) (byte) (weatherSpec.currentTemp - 273)));
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
buf.put(weatherMessage);
return buf.array();
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
if (weatherSpec == null) {
return new GBDeviceEvent[]{null};
}
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodeMarioWeatherMessage(weatherSpec);
return new GBDeviceEvent[]{sendBytes};
}
@Override
public byte[] encodeUpdateWeather(WeatherSpec weatherSpec) {
return encodeMarioWeatherMessage(weatherSpec);
}
}

View File

@ -22,21 +22,21 @@ import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class AppMessageHandlerMisfit extends AppMessageHandler {
class AppMessageHandlerMisfit extends AppMessageHandler {
public static final int KEY_SLEEPGOAL = 1;
public static final int KEY_STEP_ROGRESS = 2;
public static final int KEY_SLEEP_PROGRESS = 3;
public static final int KEY_VERSION = 4;
public static final int KEY_SYNC = 5;
public static final int KEY_INCOMING_DATA_BEGIN = 6;
public static final int KEY_INCOMING_DATA = 7;
public static final int KEY_INCOMING_DATA_END = 8;
public static final int KEY_SYNC_RESULT = 9;
private static final int KEY_SLEEPGOAL = 1;
private static final int KEY_STEP_ROGRESS = 2;
private static final int KEY_SLEEP_PROGRESS = 3;
private static final int KEY_VERSION = 4;
private static final int KEY_SYNC = 5;
private static final int KEY_INCOMING_DATA_BEGIN = 6;
private static final int KEY_INCOMING_DATA = 7;
private static final int KEY_INCOMING_DATA_END = 8;
private static final int KEY_SYNC_RESULT = 9;
private static final Logger LOG = LoggerFactory.getLogger(AppMessageHandlerMisfit.class);
public AppMessageHandlerMisfit(UUID uuid, PebbleProtocol pebbleProtocol) {
AppMessageHandlerMisfit(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
}

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