mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-12 13:09:24 +01:00
Merge branch 'master' into new_GUI
# Conflicts: # app/build.gradle
This commit is contained in:
commit
176cf79cc1
10
.github/ISSUE_TEMPLATE.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE.md
vendored
Normal 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:
|
72
CHANGELOG.md
72
CHANGELOG.md
@ -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)
|
||||
|
@ -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
68
CONTRIBUTORS.rst
Normal 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*
|
@ -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");
|
||||
|
14
README.md
14
README.md
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
steps += stepsDelta;
|
||||
lastTimestamp = timestamp;
|
||||
} else {
|
||||
// TODO: handle new day?
|
||||
|
||||
}
|
||||
} 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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
@Override
|
||||
int getGoal() {
|
||||
GBDevice device = getChartsHost().getDevice();
|
||||
if (device != null) {
|
||||
return MiBandCoordinator.getFitnessGoal(device.getAddress());
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
int getTotalForActivityAmounts(ActivityAmounts activityAmounts) {
|
||||
int totalSteps = 0;
|
||||
for (ActivityAmount amount : activityAmounts.getAmounts()) {
|
||||
totalSteps += amount.getTotalSteps();
|
||||
amount.getTotalSteps();
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,173 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
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));
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
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,10 +204,60 @@ 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;
|
||||
}
|
||||
try {
|
||||
return uriHelper.openInputStream();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
LOG.warn("file not found: " + e);
|
||||
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 {
|
||||
if (zis != null) {
|
||||
zis.close();
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
// ignore
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)) {
|
||||
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.");
|
||||
}
|
||||
lastStatecSpec = stateSpec;
|
||||
LOG.info("Got state changed intent, but not enough has changed, ignoring.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,9 +349,6 @@ public class NotificationListener extends NotificationListenerService {
|
||||
MediaController c;
|
||||
try {
|
||||
c = new MediaController(getApplicationContext(), (MediaSession.Token) extras.get(Notification.EXTRA_MEDIA_SESSION));
|
||||
} catch (NullPointerException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PlaybackState s = c.getPlaybackState();
|
||||
stateSpec.position = (int) (s.getPosition() / 1000);
|
||||
@ -385,11 +391,33 @@ public class NotificationListener extends NotificationListenerService {
|
||||
GBApplication.deviceService().onSetMusicState(stateSpec);
|
||||
|
||||
return true;
|
||||
} catch (NullPointerException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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() + ")";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -13,6 +13,8 @@ public enum DeviceType {
|
||||
MIBAND2(11),
|
||||
VIBRATISSIMO(20),
|
||||
LIVEVIEW(30),
|
||||
HPLUS(40),
|
||||
MAKIBESF68(41),
|
||||
TEST(1000);
|
||||
|
||||
private final int key;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,13 +270,12 @@ 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);
|
||||
}
|
||||
}
|
||||
} 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)) {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 synchronized String lookup(UUID uuid, String fallback) {
|
||||
if (GATTCHARACTERISTIC_DEBUG == null) {
|
||||
GATTCHARACTERISTIC_DEBUG = initDebugMap();
|
||||
}
|
||||
|
||||
public static String lookup(UUID uuid, String fallback) {
|
||||
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") + ")";
|
||||
}
|
||||
|
@ -0,0 +1,66 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,547 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,828 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
/*
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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();
|
||||
private GregorianCalendar getLastSuccessfulSyncTime() {
|
||||
long timeStampMillis = GBApplication.getPrefs().getLong(getLastSyncTimeKey(), 0);
|
||||
if (timeStampMillis != 0) {
|
||||
GregorianCalendar calendar = BLETypeConversions.createCalendar();
|
||||
calendar.setTimeInMillis((long) timestamp * 1000);
|
||||
calendar.setTimeInMillis(timeStampMillis);
|
||||
return calendar;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error querying for latest activity sample, synchronizing the last 10 days", ex);
|
||||
}
|
||||
|
||||
GregorianCalendar calendar = BLETypeConversions.createCalendar();
|
||||
calendar.add(Calendar.DAY_OF_MONTH, -10);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user