1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-26 18:45:49 +01:00

Merge branch 'master' of codeberg.org:Freeyourgadget/Gadgetbridge

This commit is contained in:
Gordon Williams 2020-08-17 10:59:11 +01:00
commit d98f8d7484
341 changed files with 20403 additions and 1878 deletions

View File

@ -5,9 +5,9 @@ about: Create a report to help us improve
---
#### Before reporting a bug, please confirm the following:
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
- [ ] I have read the [wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [Codeberg Terms of Use](https://codeberg.org/codeberg/org/src/branch/master/TermsOfUse.md)
### I got Gadgetbridge from:
* [ ] F-Droid
@ -16,7 +16,7 @@ about: Create a report to help us improve
If you got it from Google Play, please note [that version](https://github.com/TaaviE/Gadgetbridge) is unofficial and not supported here; it's also often quite outdated. Please switch to one of the above versions if you can.
#### Your issue is:
*If possible, please attach [logs](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.*
*If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.*
#### Your wearable device is:

View File

@ -5,9 +5,9 @@ about: Create a report to help us improve
---
#### Before reporting a bug, please confirm the following:
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
- [ ] I have read the [wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [Codeberg Terms of Use](https://codeberg.org/codeberg/org/src/branch/master/TermsOfUse.md)
### I got Gadgetbridge from:
* [ ] F-Droid
@ -16,7 +16,7 @@ about: Create a report to help us improve
If you got it from Google Play, please note [that version](https://github.com/TaaviE/Gadgetbridge) is unofficial and not supported here; it's also often quite outdated. Please switch to one of the above versions if you can.
#### Your issue is:
*If possible, please attach [logs](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.*
*If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.*
#### Your wearable device is:

View File

@ -5,12 +5,12 @@ about: Suggest an idea for this project
---
#### Before requesting a new feature, please confirm the following:
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
- [ ] I have read the [wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [Codeberg Terms of Use](https://codeberg.org/codeberg/org/src/branch/master/TermsOfUse.md)
#### Your issue is:
*If applicable, please attach [logs](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Log-Files)*
*If applicable, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)*
#### Your wearable device is:

View File

@ -19,10 +19,10 @@ android:
- tools
# The BuildTools version used by your project
- build-tools-28.0.3
- build-tools-29.0.3
# The SDK version used to compile your project
- android-28
- android-29
# Additional components
- extra-android-m2repository

View File

@ -1,4 +1,90 @@
### Changelog
#### 0.46.0
* Initial support for Mi Band 5
* Initial support for TLW64
* Amazfit GTR/GTS: Fix broken activity data on newer firmwares
* Big refactoring of the device discovery activity (See PR #1927 description for details)
* Add about screen
* New icon for Amazfit Bip
* Avoid duplicated entries in preferred media player selection
* Avoid a lot of crashes and improve error handling in various places
#### 0.45.1
* Amazfit GTR/GTS: Fix connection issue with latest firmwares (probably other Huami devices also affected)
* Add experimental support for TinyWeatherForecastGermany
#### 0.45.0
* Initial support for Amazfit T-Rex
* Amazfit Bip S: Support installation of latest .res
* Amazfit Bip S: Support longer notification messages
* Huami: Limit weather forecast to 7 days to fix problems with weather notificaon 0.3.11
* Huami: Improve music playback information
* Huami: Ensure cutting strings on UTF-8 border
* Stop incoming call notification when VoIP calls are missed
* Fix a crash when with Farsi translation
#### 0.44.2
* Huami: Support flashing newer GPS firmware and GPS ALM
* Amazfit Bip S: Support music control
* Amazfit Bip S: Support flashing firmware, res, gps firmware, watchfaces, fonts and GPS CEP
* Amazfit Bip S: Allow setting high MTU (much faster firmware installation, default off since it does not work for some)
* Amazfit Bip S: remove disconnect notification and button action settings (they do not work)
* Mi Band 4 (possibly others): Fix detected RES version being always 69 for non-whitelisted res files
* Fossil Hybrid HR: Add last notification widget
* Try to fix vanishing incoming call information when VoIP call support is enabled
* Allow setting device aliases (useful if you manage multiple ones of the same type)
#### Version 0.44.1
* Amazfit Bip S: Support setting shortcuts
* Amazfit Bip S: Fix setting display items
* Amazfit Bip S: Fix incoming call notification
* Huami: Fix menu items vanishing from the device when they were never configured through Gadgetbridge
* Lenovo Watch9: Fix launch of wrong calibration activity
* Reduce calls to onSetMusicInfo/onSetMusicState when playing music
#### Version 0.44.0
* Initial support for WatchX(Plus)
* Add support for Amazfit GTR Lite (untested and incomplete)
* Fossil Hybrid HR: Fix some issues with custom widgets
* Fossil Hybrid HR: Allow setting alarm titles and descriptions
* Fossil Hybrid HR: Fix step data parsing
* Amazfit GTS: Fix setting menu items with low MTU
* Amazfit GTR: Allow setting menu item like GTS
* ZeTime: Support setting the watch language
* ZeTime: Support rejecting calls
* ZeTime: Try to fix weather conditions on newer firmware
* ZeTime: Fix could not synchronize calendar on connect
* ZeTime: Fix calendar event time and date
* ZeTime: Send up to 16 upcoming calendar events on connect if option is enabled
* Allow set light/dark theme according to system settings (new default)
#### Version 0.43.3
* Fossil Hybrid HR: Initial support for activity tracking (no sleep yet)
* Fossil Hybrid HR: Support setting alarms on newer firmware
* Amazfit GTR/GTS: Fix flashing watchfaces and maybe firmware/res update (still untested)
* Amazfit GTS: Support enabling/disabling menu items on the watch
* Implement transliteration for Korean
#### Version 0.43.2
* Fossil Hybrid HR: Allow choosing and cropping image to be set as watch background
* Fossil Hybrid HR: Option to draw circles around widgets
* Fossil Hybrid HR: Experimenal firmware update support
* Fossil Hybrid HR: Fix vibration strength setting
* Huami: Do not display firmware information and whitelist information when flashing watchfaces
* Huami: Disable air quality indicator on Huami devices instead of showing 0
* Bangle.js: Change encoded char set to match Espruino's 8 bit fonts
* Steps/Sleep averages: Skip days with zero data
#### Version 0.43.1
* Initial support for Amazfit Bip S (incomplete, needs the official app once to obtain the pairing key)
* Amazift Bip Lite: Allow relaxing firmware checks to allow flashing of the regular Bip firmware (for the brave)
* Fossil Hybrid HR: Fix notification history on newer firmwares
* Fossil Hybrid HR: Add option to disable widget circle
* Bangle.js: Don't set time if the option is turned off in settings
* Bangle.js: DST and time zone fixes
* Add Arabic-style Eastern Arabic numerals to transliteration
#### Version 0.43.0
* Initial support for Fossil Hybrid HR (needs complicated key extraction, read wiki)
* Fossil: Allow switching off the Q Icon and use the default Gadgetbridge icon

View File

@ -1,28 +1,28 @@
## Feature Matrix
| | Pebble OG | Pebble Time/2 | Mi Band | Mi Band 2 | Mi Band 3 | Amazfit Bip | Amazfit Cor |
|-----------------------------------| ----------|---------------|---------|-----------|-----------|-------------|-------------|
|Calls Notification | YES | YES | YES | YES | YES | YES | YES |
|Reject Calls | YES | YES | NO | NO | YES | YES | YES |
|Accept Calls | NO(2) | NO(2) | NO | NO | NO | NO | NO |
|Generic Notification | YES | YES | YES | YES | YES | YES | YES |
|Dismiss Notifications on Phone | YES | YES | NO | NO | NO | NO | NO |
|Predefined Replies | YES | YES | NO | NO | NO | NO | NO |
|Voice Replies | N/A | NO(3) | N/A | N/A | N/A | N/A | N/A |
|Calendar Sync | YES | YES | NO | NO | NO | NO(3) | NO |
|Configure alarms from Gadgetbridge | NO | NO | YES | YES | YES | YES | YES |
|Smart alarms | NO(1) | YES | YES | NO | NO | NO | NO |
|Weather | NO(1) | YES | NO | NO | YES | YES | YES |
|Activity Tracking | NO(1) | YES | YES | YES | YES | YES | YES |
|GPS tracks import | NO | NO | NO | NO | NO | YES | NO |
|Sleep Tracking | NO(1) | YES | YES | YES | YES | YES | YES |
|HR Tracking | N/A | YES | YES | YES | YES | YES | YES |
|Realtime Activity Tracking | NO | NO | YES | YES | YES | YES | YES |
|Music Control | YES | YES | NO | NO | NO | NO | YES |
|Watchapp/face Installation | YES | YES | NO | NO | NO | YES | YES |
|Firmware Installation | YES | YES | YES | YES | YES | YES | YES |
|Taking Screenshots | YES | YES | NO | NO | NO | NO | NO |
|Support Android Companion Apps | YES | YES | NO | NO | NO | NO | NO |
| | Pebble OG | Pebble Time/2 | Mi Band | Mi Band 2 | Mi Band 3 | Mi Band 4/5 | Amazfit Bip | Amazfit Cor |
|-----------------------------------| ----------|---------------|---------|-----------|-----------|-------------|-------------|-------------|
|Calls Notification | YES | YES | YES | YES | YES | YES | YES | YES |
|Reject Calls | YES | YES | NO | NO | YES | YES | YES | YES |
|Accept Calls | NO(2) | NO(2) | NO | NO | NO | NO | NO | NO |
|Generic Notification | YES | YES | YES | YES | YES | YES | YES | YES |
|Dismiss Notifications on Phone | YES | YES | NO | NO | NO | NO | NO | NO |
|Predefined Replies | YES | YES | NO | NO | NO | NO | NO | NO |
|Voice Replies | N/A | NO(3) | N/A | N/A | N/A | N/A | N/A | N/A |
|Calendar Sync | YES | YES | NO | NO | NO | NO | NO(3) | NO |
|Configure alarms from Gadgetbridge | NO | NO | YES | YES | YES | YES(1) | YES | YES |
|Smart alarms | NO(1) | YES | YES | NO | NO | NO | NO | NO |
|Weather | NO(1) | YES | NO | NO | YES | YES | YES | YES |
|Activity Tracking | NO(1) | YES | YES | YES | YES | YES | YES | YES |
|GPS tracks import | NO | NO | NO | NO | NO | NO | YES | NO |
|Sleep Tracking | NO(1) | YES | YES | YES | YES | YES | YES | YES |
|HR Tracking | N/A | YES | YES | YES | YES | YES | YES | YES |
|Realtime Activity Tracking | NO | NO | YES | YES | YES | YES | YES | YES |
|Music Control | YES | YES | NO | NO | NO | YES | NO | YES |
|Watchapp/face Installation | YES | YES | NO | NO | NO | YES | YES | YES |
|Firmware Installation | YES | YES | YES | YES | YES | YES | YES | YES |
|Taking Screenshots | YES | YES | NO | NO | NO | NO | NO | NO |
|Support Android Companion Apps | YES | YES | NO | NO | NO | NO | NO | NO |
(1) Possible via 3rd Party Watchapp
(2) Theoretically possible (works on iOS, would need lot of work)

View File

@ -6,9 +6,9 @@ archivesBaseName = 'gadgetbridge-daogenerator'
//version = '0.9.2-SNAPSHOT'
dependencies {
// compile 'org.greenrobot:greendao-generator:2.2.0'
// compile project(":DaoGenerator")
compile 'com.github.Freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
// implementation 'org.greenrobot:greendao-generator:2.2.0'
// implementation project(":DaoGenerator")
implementation 'com.github.Freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
}
sourceSets {

View File

@ -43,7 +43,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
Schema schema = new Schema(24, MAIN_PACKAGE + ".entities");
Schema schema = new Schema(30, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -71,6 +71,11 @@ public class GBDaoGenerator {
addZeTimeActivitySample(schema, user, device);
addID115ActivitySample(schema, user, device);
addJYouActivitySample(schema, user, device);
addWatchXPlusHealthActivitySample(schema, user, device);
addWatchXPlusHealthActivityKindOverlay(schema, user, device);
addTLW64ActivitySample(schema, user, device);
addHybridHRActivitySample(schema, user, device);
addCalendarSyncState(schema, device);
addAlarms(schema, user, device);
@ -78,7 +83,7 @@ public class GBDaoGenerator {
addNotificationFilterEntry(schema, notificationFilter);
addBipActivitySummary(schema, user, device);
addActivitySummary(schema, user, device);
new DaoGenerator().generateAll(schema, "app/src/main/java");
}
@ -167,6 +172,7 @@ public class GBDaoGenerator {
device.addStringProperty("identifier").notNull().unique().javaDocGetterAndSetter("The fixed identifier, i.e. MAC address of the device.");
device.addIntProperty("type").notNull().javaDocGetterAndSetter("The DeviceType key, i.e. the GBDevice's type.");
device.addStringProperty("model").javaDocGetterAndSetter("An optional model, further specifying the kind of device-");
device.addStringProperty("alias");
Property deviceId = deviceAttributes.addLongProperty("deviceId").notNull().getProperty();
// sorted by the from-date, newest first
Property deviceAttributesSortProperty = getPropertyByName(deviceAttributes, VALID_FROM_UTC);
@ -341,6 +347,63 @@ public class GBDaoGenerator {
return activitySample;
}
private static Entity addHybridHRActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "HybridHRActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractHybridHRActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty("calories").notNull();
activitySample.addIntProperty("variability").notNull();
activitySample.addIntProperty("max_variability").notNull();
activitySample.addIntProperty("heartrate_quality").notNull();
activitySample.addBooleanProperty("active").notNull();
activitySample.addByteProperty("wear_type").notNull();
addHeartRateProperties(activitySample);
return activitySample;
}
private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addByteArrayProperty("rawWatchXPlusHealthData");
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 addWatchXPlusHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
Entity activityOverlay = addEntity(schema, "WatchXPlusHealthActivityOverlay");
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("rawWatchXPlusHealthData");
return activityOverlay;
}
private static Entity addTLW64ActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "TLW64ActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
return activitySample;
}
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
@ -391,6 +454,8 @@ public class GBDaoGenerator {
alarm.addIntProperty("hour").notNull();
alarm.addIntProperty("minute").notNull();
alarm.addBooleanProperty("unused").notNull();
alarm.addStringProperty("title");
alarm.addStringProperty("description");
alarm.addToOne(user, userId);
alarm.addToOne(device, deviceId);
}
@ -419,7 +484,7 @@ public class GBDaoGenerator {
return notificatonFilter;
}
private static void addBipActivitySummary(Schema schema, Entity user, Entity device) {
private static void addActivitySummary(Schema schema, Entity user, Entity device) {
Entity summary = addEntity(schema, "BaseActivitySummary");
summary.implementsInterface(ACTIVITY_SUMMARY);
summary.addIdProperty();
@ -442,6 +507,8 @@ public class GBDaoGenerator {
summary.addToOne(device, deviceId);
Property userId = summary.addLongProperty("userId").notNull().codeBeforeGetter(OVERRIDE).getProperty();
summary.addToOne(user, userId);
summary.addStringProperty("summaryData");
summary.addByteArrayProperty("rawSummaryData");
}
private static Property findProperty(Entity entity, String propertyName) {

View File

@ -25,3 +25,6 @@ Creative Commons Attribution 3.0 Unported license (CC BY-3.0):
Creative Commons Attribution 3.0 United States (CC BY-3.0 US):
ic_donate by Peter van Driel https://thenounproject.com/term/donate/239009/
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
ic_device_amazfit_bip by Michael quelbs@gmail.com

View File

@ -27,13 +27,15 @@ vendor's servers.
[List of changes](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/CHANGELOG.md)
## Supported Devices (Some of them WIP and some of them without maintainer)
## Supported Devices (WARNING: Some of them WIP and some of them without maintainer)
* Amazfit Bip [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
* Amazfit Bip Lite (WARNING: NEEDS MI FIT WITH ACCOUNT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-Lite)
* Amazfit Bip S (WARNING: NEEDS MI FIT WITH ACCOUNT ONCE)
* Amazfit Cor [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor)
* Amazfit Cor 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor-2)
* Amazfit GTR (WARNING: NEEDS MI FIT WITH ACCOUNT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR)
* Amazfit GTS (WARNING: NEEDS MI FIT WITH ACCOUNT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS)
* Amazfit T-Rex (WARNING: NEEDS MI FIT WITH ACCOUNT ONCE)
* BFH-16
* Casio GB-6900B
* Fossil Hybrid HR (WARNING: NEEDS FOSSIL APP WITH ACCOUNT ONCE AND COMPLICATED PROCEDURE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Fossil-Hybrid-HR)
@ -43,17 +45,20 @@ vendor's servers.
* ID115
* JYou Y5
* Lenovo Watch 9
* Lenovo Watch X (Plus) [Wiki](https://codeberg.org/mamutcho/Gadgetbridge/wiki)
* Liveview
* Makibes HR3
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
* Mi Band 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2)
* Mi Band 3 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3)
* Mi Band 4 (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4)
* Mi Band 4 (WARNING: NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4)
* Mi Band 5 (WARNING: NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-5)
* Mi Scale 2 (Currently only displays a toast after stepping on the scale)
* NO.1 F1
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Pebble 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Teclast H10, H30
* TLW64
* XWatch (Affordable Chinese Casio-like smartwatches)
* Vibratissimo (Experimental)
* ZeTime [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
@ -78,6 +83,7 @@ Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/ma
* Sebastian Kranz (ZeTime)
* Vadim Kaushan (ID115)
* "maxirnilian" (Lenovo Watch 9)
* "ksiwczynski", "mkusnierz", "mamutcho" (Lenovo Watch X Plus)
* Andreas Böhler (Casio GB-6900B)
* Jean-François Greffier (Mi Scale 2)
* Johannes Schmitt (BFH-16)
@ -86,6 +92,7 @@ Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/ma
* Gordon Williams (Bangle.js)
* Pavel Elagin (JYou Y5)
* Taavi Eomäe (iTag)
* Erik Bloß (TLW64)
## Contribute

View File

@ -16,17 +16,17 @@ android {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion 29
buildToolsVersion '29.0.3'
defaultConfig {
applicationId "nodomain.freeyourgadget.gadgetbridge"
minSdkVersion 19
targetSdkVersion 28
targetSdkVersion 29
// Note: always bump BOTH versionCode and versionName!
versionName "0.43.0"
versionCode 169
versionName "0.46.0"
versionCode 178
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@ -68,7 +68,7 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.legacy:legacy-support-v4:1.0.0"

View File

@ -31,4 +31,13 @@
-keep class **$Properties
-keep class **$Properties { *; }
-keep class **$Properties { *; }
-keep class **.gadgetbridge.database.schema.* { *; }
# Keep dependency android-emojify (io.wax911.emojify) uses
-keep class org.hamcrest.** { *; }
# Keep logback classes
-keep class ch.qos.** { *; }
-keep class org.slf4j.** { *; }

View File

@ -1,33 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="nodomain.freeyourgadget.gadgetbridge">
<!--
Comment in for testing Pebble Emulator
<uses-permission android:name="android.permission.INTERNET" />
-->
<!-- Used for Bluetooth access -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Since Android 10 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<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="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission
android:name="android.permission.MEDIA_CONTENT_CONTROL"
tools:ignore="ProtectedPermissions" />
<!-- Used for background service -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" /> <!-- Used for reverse find device -->
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<!-- Used for reverse find device -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Used for weather provider access -->
<uses-permission android:name="cyanogenmod.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="cyanogenmod.permission.READ_WEATHER" />
<uses-permission android:name="lineageos.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="lineageos.permission.READ_WEATHER" />
<uses-permission android:name="org.omnirom.omnijaws.READ_WEATHER" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="true" />
@ -37,6 +57,9 @@
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.software.companion_device_setup"
android:required="false" />
<application
android:name=".GBApplication"
@ -46,7 +69,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/GadgetbridgeTheme">
<activity android:name=".devices.qhybrid.WidgetSettingsActivity"></activity>
<activity android:name=".devices.qhybrid.WidgetSettingsActivity" />
<activity
android:name=".activities.ControlCenterv2"
android:label="@string/title_activity_controlcenter"
@ -292,6 +315,7 @@
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!-- to receive firmwares from the download content provider if recognized as zip -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -301,6 +325,7 @@
<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" />
@ -345,13 +370,21 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.BluetoothStateChangeReceiver"
android:exported="false">
android:exported="true"
android:permission="android.permission.BLUETOOTH,android.permission.BLUETOOTH_ADMIN">
<intent-filter>
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<action android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
<action android:name="android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED" />
</intent-filter>
</receiver>
<receiver
android:name=".service.receivers.GBMusicControlReceiver"
android:exported="false">
@ -369,7 +402,7 @@
<receiver
android:name=".database.PeriodicExporter"
android:enabled="true"
android:exported="false"></receiver>
android:exported="false" />
<!--
forcing the DebugActivity to portrait mode avoids crashes with the progress
dialog when changing orientation
@ -380,6 +413,19 @@
android:parentActivityName=".activities.ControlCenterv2"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.AboutActivity"
android:label="@string/about_activity_title"
android:parentActivityName=".activities.ControlCenterv2"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.ActivitySummaryDetail"
android:label="@string/activity_summary_detail"
android:parentActivityName=".activities.ActivitySummariesActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.DbManagementActivity"
android:label="@string/title_activity_db_management"
@ -405,6 +451,12 @@
<activity
android:name=".devices.watch9.Watch9CalibrationActivity"
android:label="@string/title_activity_watch9_calibration" />
<activity
android:name=".devices.lenovo.LenovoWatchPairingActivity"
android:label="@string/title_activity_watch9_pairing" />
<activity
android:name=".devices.lenovo.LenovoWatchCalibrationActivity"
android:label="@string/title_activity_LenovoWatch_calibration" />
<activity
android:name=".activities.charts.ChartsActivity"
android:label="@string/title_activity_charts"
@ -438,6 +490,7 @@
android:name=".contentprovider.PebbleContentProvider"
android:authorities="com.getpebble.android.provider"
android:exported="true" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.screenshot_provider"
@ -498,6 +551,7 @@
<data android:scheme="gadgetbridge" />
</intent-filter>
</activity>
<activity
android:name=".devices.qhybrid.ConfigActivity"
android:exported="true" />
@ -507,6 +561,8 @@
<activity
android:name=".devices.qhybrid.HRConfigActivity"
android:exported="true" />
<activity
android:name=".devices.qhybrid.ImageEditActivity"
android:exported="true" />
</application>
</manifest>

View File

@ -22,6 +22,7 @@ import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
import android.app.UiModeManager;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
@ -42,6 +43,7 @@ import android.provider.ContactsContract.PhoneLookup;
import android.util.Log;
import android.util.TypedValue;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.File;
@ -81,16 +83,13 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITBIP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITCOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITCOR2;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.HPLUS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.ID115;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND2;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND3;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND4;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.ZETIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.fromKey;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID;
/**
* Main Application class that initializes and provides access to certain things like
* logging and DB access.
@ -107,6 +106,9 @@ public class GBApplication extends Application {
private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 7;
private static final int ERROR_IN_GADGETBRIDGE_NOTIFICATION = 42;
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
private static Prefs prefs;
private static GBPrefs gbPrefs;
@ -211,7 +213,7 @@ public class GBApplication extends Application {
notificationManager.createNotificationChannel(channel);
}
NotificationChannel channelHighPr = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID );
NotificationChannel channelHighPr = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID);
if (channelHighPr == null) {
channelHighPr = new NotificationChannel(NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID,
getString(R.string.notification_channel_high_priority_name),
@ -222,7 +224,23 @@ public class GBApplication extends Application {
bluetoothStateChangeReceiver = new BluetoothStateChangeReceiver();
registerReceiver(bluetoothStateChangeReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
}
startService(new Intent(this, NotificationCollectorMonitorService.class));
try {
startService(new Intent(this, NotificationCollectorMonitorService.class));
} catch (IllegalStateException e) {
String message = e.toString();
if (message == null) {
message = getString(R.string._unknown_);
}
notificationManager.notify(ERROR_IN_GADGETBRIDGE_NOTIFICATION,
new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID)
.setSmallIcon(R.drawable.gadgetbridge_img)
.setContentTitle(getString(R.string.error_background_service))
.setContentText(getString(R.string.error_background_service_reason_truncated))
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(getString(R.string.error_background_service_reason) + "\"" + message + "\""))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build());
}
}
}
@ -855,6 +873,9 @@ public class GBApplication extends Application {
case MIBAND4:
newWearside = prefs.getString("mi_wearside", "left");
break;
case MIBAND5:
newWearside = prefs.getString("mi_wearside", "left");
break;
case HPLUS:
newWearside = prefs.getString("hplus_wrist", "left");
newTimeformat = prefs.getString("hplus_timeformat", "24h");
@ -937,7 +958,13 @@ public class GBApplication extends Application {
}
public static boolean isDarkThemeEnabled() {
return prefs.getString("pref_key_theme", context.getString(R.string.pref_theme_value_light)).equals(context.getString(R.string.pref_theme_value_dark));
String selectedTheme = prefs.getString("pref_key_theme", context.getString(R.string.pref_theme_value_system));
UiModeManager umm = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
return selectedTheme.equals(context.getString(R.string.pref_theme_value_dark)) ||
(selectedTheme.equals(context.getString(R.string.pref_theme_value_system))
&& (umm.getNightMode() == UiModeManager.MODE_NIGHT_YES));
}
public static int getTextColor(Context context) {

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2015-2020 abettenburg, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
public class AboutActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(AboutActivity.class);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
TextView link1 = (TextView) findViewById(R.id.links1);
link1.setMovementMethod(LinkMovementMethod.getInstance());
TextView link2 = (TextView) findViewById(R.id.links2);
link2.setMovementMethod(LinkMovementMethod.getInstance());
TextView link3 = (TextView) findViewById(R.id.links3);
link3.setMovementMethod(LinkMovementMethod.getInstance());
}
}

View File

@ -35,6 +35,10 @@ public abstract class AbstractListActivity<T> extends AbstractGBActivity {
this.itemAdapter.loadItems();
}
public void setActivityKindFilter(int activityKind){
this.itemAdapter.setActivityKindFilter(activityKind);
}
public AbstractItemAdapter<T> getItemAdapter() {
return itemAdapter;
}

View File

@ -32,8 +32,10 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.DatePicker;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.core.content.FileProvider;
@ -46,6 +48,7 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
@ -54,15 +57,22 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ActivitySummariesActivity extends AbstractListActivity<BaseActivitySummary> {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesActivity.class);
private GBDevice mGBDevice;
private SwipeRefreshLayout swipeLayout;
LinkedHashMap<String , Integer> activityKindMap = new LinkedHashMap<>(1);
int activityFilter=0;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
@ -121,7 +131,8 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
super.onCreate(savedInstanceState);
setItemAdapter(new ActivitySummariesAdapter(this, mGBDevice));
setItemAdapter(new ActivitySummariesAdapter(this, mGBDevice,activityFilter));
getItemListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
@ -129,13 +140,12 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
Object item = parent.getItemAtPosition(position);
if (item != null) {
ActivitySummary summary = (ActivitySummary) item;
String gpxTrack = summary.getGpxTrack();
if (gpxTrack != null) {
showTrack(gpxTrack);
} else {
GB.toast("This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.INFO);
try {
showActivityDetail(position);
} catch (Exception e) {
GB.toast(getApplicationContext(), "Unable to display Activity Detail, maybe the activity is not available yet: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
}
});
@ -230,6 +240,52 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
fetchTrackData();
}
});
activityKindMap = fillKindMap();
addItemsOnSpinner();
addListenerOnSpinnerItemSelection();
}
private LinkedHashMap fillKindMap(){
LinkedHashMap<String , Integer> newMap = new LinkedHashMap<>(1); //reset
newMap.put("All Activities", 0);
for (BaseActivitySummary item : getItemAdapter().getItems()) {
String activityName = ActivityKind.asString(item.getActivityKind(), this);
if (!newMap.containsKey(item.getActivityKind())) {
newMap.put(activityName, item.getActivityKind());
}
}
return newMap;
}
public void addListenerOnSpinnerItemSelection() {
Spinner spinner = (Spinner) findViewById(R.id.select_kind);
spinner.setOnItemSelectedListener(new CustomOnItemSelectedListener());
}
public class CustomOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
public void onItemSelected(AdapterView<?> parent, View view, int pos,long id) {
activityFilter=activityKindMap.get(parent.getItemAtPosition(pos));
setActivityKindFilter(activityFilter);
refresh();
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
// TODO Auto-generated method stub
}
}
public void addItemsOnSpinner() {
Spinner spinner = (Spinner) findViewById(R.id.select_kind);
ArrayList<String> spinnerArray = new ArrayList<>(activityKindMap.keySet());
ArrayAdapter<String> dataAdapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_dropdown_item, spinnerArray);
spinner.setAdapter(dataAdapter);
}
public void resetFetchTimestampToChosenDate() {
@ -263,12 +319,13 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
refresh();
}
private void showTrack(String gpxTrack) {
try {
AndroidUtils.viewFile(gpxTrack, Intent.ACTION_VIEW, this);
} catch (IOException e) {
GB.toast(this, "Unable to display GPX track: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
private void showActivityDetail(int position){
Intent ActivitySummaryDetailIntent = new Intent(this, ActivitySummaryDetail.class);
ActivitySummaryDetailIntent.putExtra("position", position);
ActivitySummaryDetailIntent.putExtra("filter", activityFilter);
ActivitySummaryDetailIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startActivity(ActivitySummaryDetailIntent);
}
private void fetchTrackData() {
@ -300,4 +357,7 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
}
}
}

View File

@ -0,0 +1,380 @@
/* Copyright (C) 2015-2020 abettenburg, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.SwipeEvents;
public class ActivitySummaryDetail extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryDetail.class);
private GBDevice mGBDevice;
private JSONObject groupData = setGroups();
private boolean show_raw_data = false;
BaseActivitySummary currentItem = null;
private int alternateColor;
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_summary_details);
Intent intent = getIntent();
mGBDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
final int filter = intent.getIntExtra("filter",0);
final int position = intent.getIntExtra("position",0);
final ActivitySummaryItems items = new ActivitySummaryItems(this, mGBDevice, filter);
final RelativeLayout layout = findViewById(R.id.activity_summary_detail_relative_layout);
alternateColor = getAlternateColor(this);
final Animation animFadeRight;
final Animation animFadeLeft;
final Animation animBounceLeft;
final Animation animBounceRight;
animFadeRight = AnimationUtils.loadAnimation(
this,
R.anim.flyright);
animFadeLeft = AnimationUtils.loadAnimation(
this,
R.anim.flyleft);
animBounceLeft = AnimationUtils.loadAnimation(
this,
R.anim.bounceleft);
animBounceRight = AnimationUtils.loadAnimation(
this,
R.anim.bounceright);
layout.setOnTouchListener(new SwipeEvents(this) {
@Override
public void onSwipeRight() {
currentItem = items.getNextItem();
if (currentItem != null) {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
layout.startAnimation(animFadeRight);
}else{
layout.startAnimation(animBounceRight);
}
}
@Override
public void onSwipeLeft() {
currentItem = items.getPrevItem();
if (currentItem != null) {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
layout.startAnimation(animFadeLeft);
}else{
layout.startAnimation(animBounceLeft);
}
}
});
currentItem = items.getItem(position);
if (currentItem != null) {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
}
//allows long-press.switch of data being in raw form or recalculated
ImageView activity_icon = (ImageView) findViewById(R.id.item_image);
activity_icon.setOnLongClickListener(new View.OnLongClickListener() {
public boolean onLongClick(View v) {
show_raw_data=!show_raw_data;
if (currentItem != null) {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
}
return false;
}
});
}
private void makeSummaryHeader(BaseActivitySummary item){
//make view of data from main part of item
final String gpxTrack = item.getGpxTrack();
Button show_track_btn = (Button) findViewById(R.id.showTrack);
show_track_btn.setVisibility(View.GONE);
if (gpxTrack != null) {
show_track_btn.setVisibility(View.VISIBLE);
show_track_btn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
try {
AndroidUtils.viewFile(gpxTrack, Intent.ACTION_VIEW, ActivitySummaryDetail.this);
} catch (IOException e) {
GB.toast(getApplicationContext(), "Unable to display GPX track: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
});
}
String activitykindname = ActivityKind.asString(item.getActivityKind(), getApplicationContext());
Date starttime = (Date) item.getStartTime();
Date endtime = (Date) item.getEndTime();
String starttimeS = DateTimeUtils.formatDateTime(starttime);
String endtimeS = DateTimeUtils.formatDateTime(endtime);
String durationhms = DateTimeUtils.formatDurationHoursMinutes((endtime.getTime() - starttime.getTime()), TimeUnit.MILLISECONDS);
ImageView activity_icon = (ImageView) findViewById(R.id.item_image);
activity_icon.setImageResource(ActivityKind.getIconId(item.getActivityKind()));
TextView activity_kind = (TextView) findViewById(R.id.activitykind);
activity_kind.setText(activitykindname);
TextView start_time = (TextView) findViewById(R.id.starttime);
start_time.setText(starttimeS);
TextView end_time = (TextView) findViewById(R.id.endtime);
end_time.setText(endtimeS);
TextView activity_duration = (TextView) findViewById(R.id.duration);
activity_duration.setText(durationhms);
}
private void makeSummaryContent (BaseActivitySummary item){
//make view of data from summaryData of item
TableLayout fieldLayout = findViewById(R.id.summaryDetails);
fieldLayout.removeAllViews(); //remove old widgets
JSONObject summarySubdata = null;
JSONObject data = null;
String sumData = item.getSummaryData();
if (sumData != null) {
try {
summarySubdata = new JSONObject(sumData);
} catch (JSONException e) {
LOG.error("SportsActivity", e);
}
}
if (summarySubdata == null) return;
data = makeSummaryList(summarySubdata); //make new list, grouped by groups
if (data == null) return;
Iterator<String> keys = data.keys();
DecimalFormat df = new DecimalFormat("#.##");
while (keys.hasNext()) {
String key = keys.next();
try {
LOG.error("SportsActivity:" + key + ": " + data.get(key) + "\n");
JSONArray innerList = (JSONArray) data.get(key);
TableRow label_row = new TableRow(ActivitySummaryDetail.this);
TextView label_field = new TextView(ActivitySummaryDetail.this);
label_field.setTextSize(16);
label_field.setTypeface(null, Typeface.BOLD);
label_field.setText(String.format("%s", getStringResourceByName(key)));
label_row.addView(label_field);
fieldLayout.addView(label_row);
for (int i = 0; i < innerList.length(); i++) {
JSONObject innerData = innerList.getJSONObject(i);
double value = innerData.getDouble("value");
String unit = innerData.getString("unit");
String name = innerData.getString("name");
if (!show_raw_data) {
//special casing here:
switch (unit) {
case "meters_second":
value = value * 3.6;
unit = "km_h";
break;
case "seconds_m":
value = 3.6 / value;
unit = "minutes_km";
break;
case "seconds_km":
value = value / 60;
unit = "minutes_km";
break;
}
}
TableRow field_row = new TableRow(ActivitySummaryDetail.this);
if (i % 2 == 0) field_row.setBackgroundColor(alternateColor);
TextView name_field = new TextView(ActivitySummaryDetail.this);
TextView value_field = new TextView(ActivitySummaryDetail.this);
name_field.setGravity(Gravity.START);
value_field.setGravity(Gravity.END);
if (unit.equals("seconds") && !show_raw_data) { //rather then plain seconds, show formatted duration
value_field.setText(DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS));
}else {
value_field.setText(String.format("%s %s", df.format(value), getStringResourceByName(unit)));
}
name_field.setText(getStringResourceByName(name));
TableRow.LayoutParams params = new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f);
value_field.setLayoutParams(params);
field_row.addView(name_field);
field_row.addView(value_field);
fieldLayout.addView(field_row);
}
} catch (JSONException e) {
LOG.error("SportsActivity", e);
}
}
}
private JSONObject setGroups(){
String groupDefinitions = "{'Strokes':['averageStrokeDistance','averageStrokesPerSecond','strokes'], " +
"'Swimming':['swolfIndex','swimStyle'], " +
"'Elevation':['ascentMeters','descentMeters','maxAltitude','minAltitude','ascentSeconds','descentSeconds','flatSeconds'], " +
"'Speed':['maxSpeed','minPace','maxPace','averageKMPaceSeconds'], " +
"'Activity':['distanceMeters','steps','activeSeconds','caloriesBurnt','totalStride'," +
"'averageHR','averageStride'], " +
"'Laps':['averageLapPace','laps']}";
JSONObject data = null;
try {
data = new JSONObject(groupDefinitions);
} catch (JSONException e) {
LOG.error("SportsActivity", e);
}
return data;
}
private String getGroup(String searchItem) {
String defaultGroup = "Activity";
if (groupData == null) return defaultGroup;
Iterator<String> keys = groupData.keys();
while (keys.hasNext()) {
String key = keys.next();
try {
JSONArray itemList = (JSONArray) groupData.get(key);
for (int i = 0; i < itemList.length(); i++) {
if (itemList.getString(i).equals(searchItem)) {
return key;
}
}
} catch (JSONException e) {
LOG.error("SportsActivity", e);
}
}
return defaultGroup;
}
private JSONObject makeSummaryList(JSONObject summaryData){
//make dictionary with data for each group
JSONObject list = new JSONObject();
Iterator<String> keys = summaryData.keys();
LOG.error("SportsActivity JSON:" + summaryData + keys);
while (keys.hasNext()) {
String key = keys.next();
try {
LOG.error("SportsActivity:" + key + ": " + summaryData.get(key) + "\n");
JSONObject innerData = (JSONObject) summaryData.get(key);
Object value = innerData.get("value");
String unit = innerData.getString("unit");
String group = getGroup(key);
if (!list.has(group)) {
list.put(group,new JSONArray());
}
JSONArray tmpl = (JSONArray) list.get(group);
JSONObject innernew = new JSONObject();
innernew.put("name", key);
innernew.put("value", value);
innernew.put("unit", unit);
tmpl.put(innernew);
list.put(group, tmpl);
} catch (JSONException e) {
LOG.error("SportsActivity", e);
}
}
return list;
}
public static int getAlternateColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(R.attr.alternate_row_background, typedValue, true);
return typedValue.data;
}
private String getStringResourceByName(String aString) {
String packageName = getPackageName();
int resId = getResources().getIdentifier(aString, "string", packageName);
if (resId==0){
LOG.warn("SportsActivity " + "Missing string in strings:" + aString);
return aString;
}else{
return getString(resId);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// back button
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -22,6 +22,7 @@ import android.text.format.DateFormat;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckedTextView;
import android.widget.EditText;
import android.widget.TimePicker;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -46,6 +47,8 @@ public class AlarmDetails extends AbstractGBActivity {
private CheckedTextView cbFriday;
private CheckedTextView cbSaturday;
private CheckedTextView cbSunday;
private EditText title;
private EditText description;
private GBDevice device;
@Override
@ -56,6 +59,9 @@ public class AlarmDetails extends AbstractGBActivity {
alarm = (Alarm) getIntent().getSerializableExtra(nodomain.freeyourgadget.gadgetbridge.model.Alarm.EXTRA_ALARM);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
title = findViewById(R.id.alarm_title);
description = findViewById(R.id.alarm_description);
timePicker = findViewById(R.id.alarm_time_picker);
cbSmartWakeup = findViewById(R.id.alarm_cb_smart_wakeup);
cbSnooze = findViewById(R.id.alarm_cb_snooze);
@ -126,6 +132,12 @@ public class AlarmDetails extends AbstractGBActivity {
int snoozeVisibility = supportsSnoozing() ? View.VISIBLE : View.GONE;
cbSnooze.setVisibility(snoozeVisibility);
int descriptionVisibility = supportsDescription() ? View.VISIBLE : View.GONE;
title.setVisibility(descriptionVisibility);
title.setText(alarm.getTitle());
description.setVisibility(descriptionVisibility);
description.setText(alarm.getDescription());
cbMonday.setChecked(alarm.getRepetition(Alarm.ALARM_MON));
cbTuesday.setChecked(alarm.getRepetition(Alarm.ALARM_TUE));
cbWednesday.setChecked(alarm.getRepetition(Alarm.ALARM_WED));
@ -144,6 +156,14 @@ public class AlarmDetails extends AbstractGBActivity {
return false;
}
private boolean supportsDescription() {
if (device != null) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator.supportsAlarmDescription(device);
}
return false;
}
private boolean supportsSnoozing() {
if (device != null) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
@ -170,6 +190,8 @@ public class AlarmDetails extends AbstractGBActivity {
alarm.setRepetition(repetitionMask);
alarm.setHour(timePicker.getCurrentHour());
alarm.setMinute(timePicker.getCurrentMinute());
alarm.setTitle(title.getText().toString());
alarm.setDescription(description.getText().toString());
DBHelper.store(alarm);
}

View File

@ -146,7 +146,7 @@ public class ConfigureAlarms extends AbstractGBActivity {
}
private Alarm createDefaultAlarm(@NonNull Device device, @NonNull User user, int position) {
return new Alarm(device.getId(), user.getId(), position, false, false, false, 0, 6, 30, false);
return new Alarm(device.getId(), user.getId(), position, false, false, false, 0, 6, 30, false, null, null);
}
@Override

View File

@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -39,6 +40,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
@ -50,9 +52,11 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import de.cketti.library.changelog.ChangeLog;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -68,23 +72,19 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class ControlCenterv2 extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener, GBActivity {
public static final int MENU_REFRESH_CODE = 1;
private static PhoneStateListener fakeStateListener;
//needed for KK compatibility
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
private DeviceManager deviceManager;
private GBDeviceAdapterv2 mGBDeviceAdapter;
private RecyclerView deviceListView;
private FloatingActionButton fab;
private boolean isLanguageInvalid = false;
public static final int MENU_REFRESH_CODE=1;
private static PhoneStateListener fakeStateListener;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -102,6 +102,7 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
};
private boolean pesterWithPermissions = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -192,11 +193,16 @@ public class ControlCenterv2 extends AppCompatActivity
* Ask for permission to intercept notifications on first run.
*/
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("firstrun", true)) {
prefs.getPreferences().edit().putBoolean("firstrun", false).apply();
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
startActivity(enableIntent);
pesterWithPermissions = prefs.getBoolean("permission_pestering", true);
Set<String> set = NotificationManagerCompat.getEnabledListenerPackages(this);
if (pesterWithPermissions) {
if (!set.contains(this.getPackageName())) { // If notification listener access hasn't been granted
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
startActivity(enableIntent);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkAndRequestPermissions();
}
@ -205,7 +211,7 @@ public class ControlCenterv2 extends AppCompatActivity
if (cl.isFirstRun()) {
try {
cl.getLogDialog().show();
} catch (Exception ignored){
} catch (Exception ignored) {
GB.toast(getBaseContext(), "Error showing Changelog", Toast.LENGTH_LONG, GB.ERROR);
}
@ -297,6 +303,10 @@ public class ControlCenterv2 extends AppCompatActivity
GB.toast(getBaseContext(), "Error showing Changelog", Toast.LENGTH_LONG, GB.ERROR);
}
return true;
case R.id.about:
Intent aboutIntent = new Intent(this, AboutActivity.class);
startActivity(aboutIntent);
return true;
}
return true;
@ -343,8 +353,6 @@ public class ControlCenterv2 extends AppCompatActivity
wantedPermissions.add(Manifest.permission.READ_CONTACTS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.CALL_PHONE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ANSWER_PHONE_CALLS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.ANSWER_PHONE_CALLS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CALL_LOG);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED)
@ -361,14 +369,69 @@ public class ControlCenterv2 extends AppCompatActivity
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, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
try {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.MEDIA_CONTENT_CONTROL) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.MEDIA_CONTENT_CONTROL);
} catch (Exception ignored){
} catch (Exception ignored) {
}
if (!wantedPermissions.isEmpty())
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (pesterWithPermissions) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ANSWER_PHONE_CALLS) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.ANSWER_PHONE_CALLS);
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
}
}
if (!wantedPermissions.isEmpty()) {
Prefs prefs = GBApplication.getPrefs();
// If this is not the first run, we can rely on
// shouldShowRequestPermissionRationale(String permission)
// and ignore permissions that shouldn't or can't be requested again
if (prefs.getBoolean("permissions_asked", false)) {
// Don't request permissions that we shouldn't show a prompt for
// e.g. permissions that are "Never" granted by the user or never granted by the system
Set<String> shouldNotAsk = new HashSet<>();
for (String wantedPermission : wantedPermissions) {
if (!shouldShowRequestPermissionRationale(wantedPermission)) {
shouldNotAsk.add(wantedPermission);
}
}
wantedPermissions.removeAll(shouldNotAsk);
} else {
// Permissions have not been asked yet, but now will be
prefs.getPreferences().edit().putBoolean("permissions_asked", true).apply();
}
if (!wantedPermissions.isEmpty()) {
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
}
}
/* In order to be able to set ringer mode to silent in GB's PhoneCallReceiver
the permission to access notifications is needed above Android M
ACCESS_NOTIFICATION_POLICY is also needed in the manifest */
if (pesterWithPermissions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!((NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted()) {
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
startActivity(new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS));
}
}
}
// HACK: On Lineage we have to do this so that the permission dialog pops up
if (fakeStateListener == null) {

View File

@ -17,7 +17,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
@ -160,9 +159,8 @@ public class DbManagementActivity extends AbstractGBActivity {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
}
}
catch (Exception fdfsdfds) {
LOG.warn("fuck");
} catch (Exception fdfsdfds) {
LOG.error("Error", fdfsdfds);
}
}
return "";
@ -205,7 +203,7 @@ public class DbManagementActivity extends AbstractGBActivity {
}
}
} catch (Exception e) {
GB.toast("Error exporting device specific preferences", Toast.LENGTH_SHORT, GB.ERROR);
GB.toast("Error exporting device specific preferences", Toast.LENGTH_SHORT, GB.ERROR, e);
}
}
@ -217,6 +215,7 @@ public class DbManagementActivity extends AbstractGBActivity {
} catch (Exception ex) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
try (DBHandler lockHandler = GBApplication.acquireDB()) {
List<Device> activeDevices = DBHelper.getActiveDevices(lockHandler.getDaoSession());
for (Device dbDevice : activeDevices) {

View File

@ -46,18 +46,20 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -392,8 +394,14 @@ public class SettingsActivity extends AbstractSettingsActivity {
newValues[0] = "default";
int i = 1;
Set<String> existingNames = new HashSet<>();
for (ResolveInfo resolveInfo : mediaReceivers) {
newEntries[i] = resolveInfo.activityInfo.loadLabel(pm);
newEntries[i] = resolveInfo.activityInfo.loadLabel(pm) + " (" + resolveInfo.activityInfo.packageName + ")";
if (existingNames.contains(newEntries[i].toString().trim())) {
newEntries[i] = resolveInfo.activityInfo.loadLabel(pm) + " (" + resolveInfo.activityInfo.name + ")";
} else {
existingNames.add(newEntries[i].toString().trim());
}
newValues[i] = resolveInfo.activityInfo.packageName;
i++;
}

View File

@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
protected final int TOTAL_DAYS = getRangeDays();
protected int TOTAL_DAYS_FOR_AVERAGE = 0;
private Locale mLocale;
private int mTargetValue = 0;
@ -124,10 +125,17 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
ArrayList<String> labels = new ArrayList<String>();
long balance = 0;
long daily_balance=0;
TOTAL_DAYS_FOR_AVERAGE=0;
for (int counter = 0; counter < TOTAL_DAYS; counter++) {
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
daily_balance=calculateBalance(amounts);
if (daily_balance>0){
TOTAL_DAYS_FOR_AVERAGE++;
}
balance += calculateBalance(amounts);
balance += daily_balance;
entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts)));
labels.add(getWeeksChartsLabel(day));
day.add(Calendar.DATE, 1);
@ -146,8 +154,8 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
barChart.getAxisLeft().addLimitLine(target);
float average = 0;
if (TOTAL_DAYS > 0) {
average = Math.abs(balance / TOTAL_DAYS);
if (TOTAL_DAYS_FOR_AVERAGE > 0) {
average = Math.abs(balance / TOTAL_DAYS_FOR_AVERAGE);
}
LimitLine average_line = new LimitLine(average);
average_line.setLabel(getString(R.string.average, getAverage(average)));

View File

@ -76,7 +76,7 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
@Override
protected String getBalanceMessage(long balance, int targetValue) {
if (balance > 0) {
final long totalBalance = balance - ((long)targetValue * TOTAL_DAYS);
final long totalBalance = balance - ((long)targetValue * TOTAL_DAYS_FOR_AVERAGE);
if (totalBalance > 0)
return getString(R.string.overslept, getHM(totalBalance));
else

View File

@ -109,7 +109,7 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
@Override
protected String getBalanceMessage(long balance, int targetValue) {
if (balance > 0) {
final long totalBalance = balance - ((long)targetValue * TOTAL_DAYS);
final long totalBalance = balance - ((long)targetValue * TOTAL_DAYS_FOR_AVERAGE);
if (totalBalance > 0)
return getString(R.string.overstep, Math.abs(totalBalance));
else

View File

@ -17,6 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
public class DeviceSettingsPreferenceConst {
public static final String PREF_LANGUAGE = "language";
public static final String PREF_DATEFORMAT = "dateformat";
public static final String PREF_TIMEFORMAT = "timeformat";
public static final String PREF_WEARLOCATION = "wearlocation";
@ -30,4 +31,18 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_BUTTON_3_FUNCTION = "button_3_function";
public static final String PREF_VIBRATION_STRENGH_PERCENTAGE = "vibration_strength";
public static final String PREF_RELAX_FIRMWARE_CHECKS = "relax_firmware_checks";
public static final String PREF_HYBRID_HR_FORCE_WHITE_COLOR = "force_white_color_scheme";
public static final String PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES = "widget_draw_circles";
public static final String PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES = "save_raw_activity_files";
public static final String PREF_LIFTWRIST_NOSHED = "activate_display_on_lift_wrist_noshed";
public static final String PREF_DISCONNECTNOTIF_NOSHED = "disconnect_notification_noshed";
public static final String PREF_POWER_MODE = "power_mode";
public static final String PREF_BUTTON_BP_CALIBRATE = "prefs_sensors_button_bp_calibration";
public static final String PREF_ALTITUDE_CALIBRATE = "pref_sensors_altitude";
public static final String PREF_LONGSIT_PERIOD = "pref_longsit_period";
public static final String PREF_LONGSIT_SWITCH = "pref_longsit_switch";
public static final String PREF_LONGSIT_SWITCH_NOSHED = "screen_longsit_noshed";
public static final String PREF_DO_NOT_DISTURB_NOAUTO = "do_not_disturb_no_auto";
}

View File

@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
@ -39,10 +40,22 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALTITUDE_CALIBRATE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BUTTON_BP_CALIBRATE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYBRID_HR_FORCE_WHITE_COLOR;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_PERIOD;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_POWER_MODE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SCREEN_ORIENTATION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE;
@ -55,7 +68,15 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_LANGUAGE;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_SHORTCUTS;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_WOKE_UP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_WOKE_UP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB_OFF;
@ -312,6 +333,7 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
addPreferenceHandlerFor(PREF_MI2_DATEFORMAT);
addPreferenceHandlerFor(PREF_DATEFORMAT);
addPreferenceHandlerFor(PREF_DISPLAY_ITEMS);
addPreferenceHandlerFor(PREF_SHORTCUTS);
addPreferenceHandlerFor(PREF_LANGUAGE);
addPreferenceHandlerFor(PREF_EXPOSE_HR_THIRDPARTY);
addPreferenceHandlerFor(PREF_WEARLOCATION);
@ -321,6 +343,19 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
addPreferenceHandlerFor(PREF_BUTTON_2_FUNCTION);
addPreferenceHandlerFor(PREF_BUTTON_3_FUNCTION);
addPreferenceHandlerFor(PREF_VIBRATION_STRENGH_PERCENTAGE);
addPreferenceHandlerFor(PREF_POWER_MODE);
addPreferenceHandlerFor(PREF_LIFTWRIST_NOSHED);
addPreferenceHandlerFor(PREF_DISCONNECTNOTIF_NOSHED);
addPreferenceHandlerFor(PREF_BUTTON_BP_CALIBRATE);
addPreferenceHandlerFor(PREF_ALTITUDE_CALIBRATE);
addPreferenceHandlerFor(PREF_LONGSIT_PERIOD);
addPreferenceHandlerFor(PREF_LONGSIT_SWITCH);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO);
addPreferenceHandlerFor(PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES);
addPreferenceHandlerFor(PREF_HYBRID_HR_FORCE_WHITE_COLOR);
addPreferenceHandlerFor(PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES);
String displayOnLiftState = prefs.getString(PREF_ACTIVATE_DISPLAY_ON_LIFT, PREF_DO_NOT_DISTURB_OFF);
boolean displayOnLiftScheduled = displayOnLiftState.equals(PREF_DO_NOT_DISTURB_SCHEDULED);
@ -404,6 +439,94 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
setInputTypeFor(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS, InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
setInputTypeFor(MakibesHR3Constants.PREF_FIND_PHONE_DURATION, InputType.TYPE_CLASS_NUMBER);
setInputTypeFor(DeviceSettingsPreferenceConst.PREF_RESERVER_ALARMS_CALENDAR, InputType.TYPE_CLASS_NUMBER);
String deviceActionsFellSleepSelection = prefs.getString(PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION, PREF_DEVICE_ACTION_SELECTION_OFF);
final String deviceActionsFellSleepBroadcastValue = prefs.getString(PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST,
this.getContext().getString(R.string.prefs_events_forwarding_fellsleep_broadcast_default_value));
final Preference deviceActionsFellSleep = findPreference(PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION);
final Preference deviceActionsFellSleepBroadcast = findPreference(PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST);
boolean deviceActionsFellSleepSelectionBroadcast = deviceActionsFellSleepSelection.equals(PREF_DEVICE_ACTION_SELECTION_BROADCAST);
if (deviceActionsFellSleep != null) {
deviceActionsFellSleep.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
final boolean broadcast = PREF_DEVICE_ACTION_SELECTION_BROADCAST.equals(newVal.toString());
Objects.requireNonNull(deviceActionsFellSleepBroadcast).setEnabled(broadcast);
return true;
}
});
}
if (deviceActionsFellSleepBroadcast != null) {
deviceActionsFellSleepBroadcast.setSummary(deviceActionsFellSleepBroadcastValue);
deviceActionsFellSleepBroadcast.setEnabled(deviceActionsFellSleepSelectionBroadcast);
deviceActionsFellSleepBroadcast.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
deviceActionsFellSleepBroadcast.setSummary(newVal.toString());
return true;
}
});
}
String deviceActionsWokeUpSelection = prefs.getString(PREF_DEVICE_ACTION_WOKE_UP_SELECTION, PREF_DEVICE_ACTION_SELECTION_OFF);
final String deviceActionsWokeUpBroadcastValue = prefs.getString(PREF_DEVICE_ACTION_WOKE_UP_BROADCAST,
this.getContext().getString(R.string.prefs_events_forwarding_wokeup_broadcast_default_value));
final Preference deviceActionsWokeUp = findPreference(PREF_DEVICE_ACTION_WOKE_UP_SELECTION);
final Preference deviceActionsWokeUpBroadcast = findPreference(PREF_DEVICE_ACTION_WOKE_UP_BROADCAST);
boolean deviceActionsWokeUpSelectionBroadcast = deviceActionsWokeUpSelection.equals(PREF_DEVICE_ACTION_SELECTION_BROADCAST);
if (deviceActionsWokeUp != null) {
deviceActionsWokeUp.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
final boolean broadcast = PREF_DEVICE_ACTION_SELECTION_BROADCAST.equals(newVal.toString());
Objects.requireNonNull(deviceActionsWokeUpBroadcast).setEnabled(broadcast);
return true;
}
});
}
if (deviceActionsWokeUpBroadcast != null) {
deviceActionsWokeUpBroadcast.setSummary(deviceActionsWokeUpBroadcastValue);
deviceActionsWokeUpBroadcast.setEnabled(deviceActionsWokeUpSelectionBroadcast);
deviceActionsWokeUpBroadcast.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
deviceActionsWokeUpBroadcast.setSummary(newVal.toString());
return true;
}
});
}
String deviceActionsStartNonWearSelection = prefs.getString(PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION, PREF_DEVICE_ACTION_SELECTION_OFF);
final String deviceActionsStartNonWearBroadcastValue = prefs.getString(PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST,
this.getContext().getString(R.string.prefs_events_forwarding_startnonwear_broadcast_default_value));
final Preference deviceActionsStartNonWear = findPreference(PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION);
final Preference deviceActionsStartNonWearBroadcast = findPreference(PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST);
boolean deviceActionsStartNonWearSelectionBroadcast = deviceActionsStartNonWearSelection.equals(PREF_DEVICE_ACTION_SELECTION_BROADCAST);
if (deviceActionsStartNonWear != null) {
deviceActionsStartNonWear.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
final boolean broadcast = PREF_DEVICE_ACTION_SELECTION_BROADCAST.equals(newVal.toString());
Objects.requireNonNull(deviceActionsStartNonWearBroadcast).setEnabled(broadcast);
return true;
}
});
}
if (deviceActionsStartNonWearBroadcast != null) {
deviceActionsStartNonWearBroadcast.setSummary(deviceActionsStartNonWearBroadcastValue);
deviceActionsStartNonWearBroadcast.setEnabled(deviceActionsStartNonWearSelectionBroadcast);
deviceActionsStartNonWearBroadcast.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
deviceActionsStartNonWearBroadcast.setSummary(newVal.toString());
return true;
}
});
}
}
static DeviceSpecificSettingsFragment newInstance(String settingsFileSuffix, @NonNull int[] supportedSettings) {

View File

@ -59,6 +59,10 @@ public abstract class AbstractItemAdapter<T> extends ArrayAdapter<T> {
this.horizontalAlignment = horizontalAlignment;
}
public void setActivityKindFilter(int activityKind){
this.setActivityKindFilter(activityKind);
}
@Override
public View getView(int position, View view, ViewGroup parent) {
T item = getItem(position);

View File

@ -21,6 +21,7 @@ import android.widget.Toast;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -36,10 +37,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ActivitySummariesAdapter extends AbstractItemAdapter<BaseActivitySummary> {
private final GBDevice device;
private int activityKindFilter;
public ActivitySummariesAdapter(Context context, GBDevice device) {
public ActivitySummariesAdapter(Context context, GBDevice device, int activityKindFilter) {
super(context);
this.device = device;
this.activityKindFilter = activityKindFilter;
loadItems();
}
@ -50,7 +53,17 @@ public class ActivitySummariesAdapter extends AbstractItemAdapter<BaseActivitySu
Device dbDevice = DBHelper.findDevice(device, handler.getDaoSession());
QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(dbDevice.getId())).orderDesc(BaseActivitySummaryDao.Properties.StartTime);
if (activityKindFilter !=0){
qb.where(
BaseActivitySummaryDao.Properties.DeviceId.eq(dbDevice.getId()),
BaseActivitySummaryDao.Properties.ActivityKind.eq(activityKindFilter))
.orderDesc(BaseActivitySummaryDao.Properties.StartTime);
}else{
qb.where(
BaseActivitySummaryDao.Properties.DeviceId.eq(
dbDevice.getId())).orderDesc(BaseActivitySummaryDao.Properties.StartTime);
}
List<BaseActivitySummary> allSummaries = qb.build().list();
setItems(allSummaries, true);
} catch (Exception e) {
@ -58,23 +71,41 @@ public class ActivitySummariesAdapter extends AbstractItemAdapter<BaseActivitySu
}
}
public void setActivityKindFilter(int filter){
this.activityKindFilter=filter;
}
@Override
protected String getName(BaseActivitySummary item) {
String name = item.getName();
if (name != null && name.length() > 0) {
return name;
}
Date startTime = item.getStartTime();
Long duration = (item.getEndTime().getTime() - item.getStartTime().getTime());
if (startTime != null) {
return DateTimeUtils.formatDateTime(startTime);
return DateTimeUtils.formatDateTime(startTime) + " (" + DateTimeUtils.formatDurationHoursMinutes(duration, TimeUnit.MILLISECONDS) + ")";
}
return "Unknown activity";
}
@Override
protected String getDetails(BaseActivitySummary item) {
return ActivityKind.asString(item.getActivityKind(), getContext());
String gpxTrack = item.getGpxTrack();
String hasGps = "";
if (gpxTrack != null) {
hasGps=" 🛰️";
}
return ActivityKind.asString(item.getActivityKind(), getContext())+ hasGps;
}
@Override

View File

@ -16,6 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@ -26,9 +27,12 @@ import android.widget.TextView;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -47,22 +51,37 @@ public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
@Override
public View getView(int position, View view, ViewGroup parent) {
GBDeviceCandidate device = getItem(position);
if (view == null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.item_with_details, parent, false);
}
ImageView deviceImageView = (ImageView) view.findViewById(R.id.item_image);
TextView deviceNameLabel = (TextView) view.findViewById(R.id.item_name);
TextView deviceAddressLabel = (TextView) view.findViewById(R.id.item_details);
ImageView deviceImageView = view.findViewById(R.id.item_image);
TextView deviceNameLabel = view.findViewById(R.id.item_name);
TextView deviceAddressLabel = view.findViewById(R.id.item_details);
TextView deviceStatus = view.findViewById(R.id.item_status);
String name = formatDeviceCandidate(device);
deviceNameLabel.setText(name);
deviceAddressLabel.setText(device.getMacAddress());
deviceImageView.setImageResource(device.getDeviceType().getIcon());
String status = "";
if (device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) {
status += getContext().getString(R.string.device_is_currently_bonded);
if (!GBApplication.getPrefs().getBoolean("ignore_bonded_devices", true)) { // This could be passed to the constructor instead
deviceImageView.setImageResource(device.getDeviceType().getDisabledIcon());
}
}
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
if (device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) {
status += "\n";
}
status += getContext().getString(R.string.device_requires_key);
}
deviceStatus.setText(status);
return view;
}

View File

@ -38,17 +38,21 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.snackbar.Snackbar;
import com.jaredrummler.android.colorpicker.ColorPickerDialog;
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener;
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import com.jaredrummler.android.colorpicker.ColorPickerDialog;
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesActivity;
@ -56,9 +60,12 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9CalibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
@ -70,6 +77,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
* Adapter for displaying GBDevice instances.
*/
public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.ViewHolder> {
private static final Logger LOG = LoggerFactory.getLogger(GBDeviceAdapterv2.class);
private final Context context;
private List<GBDevice> deviceList;
@ -319,11 +327,11 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
);
holder.calibrateDevice.setVisibility(device.isInitialized() && device.getType() == DeviceType.WATCH9 ? View.VISIBLE : View.GONE);
holder.calibrateDevice.setVisibility(device.isInitialized() && (coordinator.getCalibrationActivity() != null) ? View.VISIBLE : View.GONE);
holder.calibrateDevice.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(context, Watch9CalibrationActivity.class);
Intent startIntent = new Intent(context, coordinator.getCalibrationActivity());
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
@ -352,9 +360,9 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
float frequency = Float.valueOf(input.getText().toString());
float frequency = Float.parseFloat(input.getText().toString());
// Trim to 1 decimal place, discard the rest
frequency = Float.valueOf(String.format(Locale.getDefault(), "%.1f", frequency));
frequency = Float.parseFloat(String.format(Locale.getDefault(), "%.1f", frequency));
if (frequency < 87.5 || frequency > 108.0) {
new AlertDialog.Builder(context)
.setTitle(R.string.pref_invalid_frequency_title)
@ -468,6 +476,47 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
}
});
//set alias, hidden under details
holder.setAlias.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
final EditText input = new EditText(context);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(device.getAlias());
// Specify the type of input expected; this, for example, sets the input as a password, and will mask the text
new AlertDialog.Builder(context)
.setView(input)
.setCancelable(true)
.setTitle(context.getString(R.string.controlcenter_set_alias))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Device dbDevice = DBHelper.getDevice(device, session);
String alias = input.getText().toString();
dbDevice.setAlias(alias);
dbDevice.update();
device.setAlias(alias);
} catch (Exception ex) {
GB.toast(context, context.getString(R.string.error_setting_alias) + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
} finally {
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// do nothing
}
})
.show();
}
});
}
@Override
@ -504,6 +553,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
ListView deviceInfoList;
ImageView findDevice;
ImageView removeDevice;
ImageView setAlias;
LinearLayout fmFrequencyBox;
TextView fmFrequencyLabel;
ImageView ledColor;
@ -537,6 +587,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
deviceInfoList = view.findViewById(R.id.device_item_infos);
findDevice = view.findViewById(R.id.device_action_find);
removeDevice = view.findViewById(R.id.device_action_remove);
setAlias = view.findViewById(R.id.device_action_set_alias);
fmFrequencyBox = view.findViewById(R.id.device_fm_frequency_box);
fmFrequencyLabel = view.findViewById(R.id.fm_frequency);
ledColor = view.findViewById(R.id.device_led_color);
@ -564,7 +615,8 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
}
private String getUniqueDeviceName(GBDevice device) {
String deviceName = device.getName();
String deviceName = device.getAliasOrName();
if (!isUniqueDeviceName(device, deviceName)) {
if (device.getModel() != null) {
deviceName = deviceName + " " + device.getModel();

View File

@ -429,6 +429,7 @@ public class DBHelper {
if (!isDeviceUpToDate(device, gbDevice)) {
device.setIdentifier(gbDevice.getAddress());
device.setName(gbDevice.getName());
device.setAlias(gbDevice.getAlias());
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
device.setManufacturer(coordinator.getManufacturer());
device.setType(gbDevice.getType().getKey());
@ -449,6 +450,9 @@ public class DBHelper {
if (!Objects.equals(device.getName(), gbDevice.getName())) {
return false;
}
if (!Objects.equals(device.getAlias(), gbDevice.getAlias())) {
return false;
}
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
if (!Objects.equals(device.getManufacturer(), coordinator.getManufacturer())) {
return false;

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, protomors
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
public class GadgetbridgeUpdate_26 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(AlarmDao.TABLENAME, AlarmDao.Properties.Title.columnName, db)) {
String ADD_COLUMN_TITLE = "ALTER TABLE " + AlarmDao.TABLENAME + " ADD COLUMN "
+ AlarmDao.Properties.Title.columnName + " TEXT";
db.execSQL(ADD_COLUMN_TITLE);
}
if (!DBHelper.existsColumn(AlarmDao.TABLENAME, AlarmDao.Properties.Description.columnName, db)) {
String ADD_COLUMN_TITLE = "ALTER TABLE " + AlarmDao.TABLENAME + " ADD COLUMN "
+ AlarmDao.Properties.Description.columnName + " TEXT";
db.execSQL(ADD_COLUMN_TITLE);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, protomors
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
public class GadgetbridgeUpdate_27 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(DeviceDao.TABLENAME, DeviceDao.Properties.Alias.columnName, db)) {
String ADD_COLUMN_ALIAS = "ALTER TABLE " + DeviceDao.TABLENAME + " ADD COLUMN "
+ DeviceDao.Properties.Alias.columnName + " TEXT";
db.execSQL(ADD_COLUMN_ALIAS);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, protomors
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
public class GadgetbridgeUpdate_29 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(BaseActivitySummaryDao.TABLENAME, BaseActivitySummaryDao.Properties.SummaryData.columnName, db)) {
String ADD_COLUMN_SUMMARY_DATA = "ALTER TABLE " + BaseActivitySummaryDao.TABLENAME + " ADD COLUMN "
+ BaseActivitySummaryDao.Properties.SummaryData.columnName + " TEXT";
db.execSQL(ADD_COLUMN_SUMMARY_DATA);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, protomors
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
public class GadgetbridgeUpdate_30 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(BaseActivitySummaryDao.TABLENAME, BaseActivitySummaryDao.Properties.RawSummaryData.columnName, db)) {
String ADD_COLUMN_RAW_SUMMARY_DATA = "ALTER TABLE " + BaseActivitySummaryDao.TABLENAME + " ADD COLUMN "
+ BaseActivitySummaryDao.Properties.RawSummaryData.columnName + " BLOB";
db.execSQL(ADD_COLUMN_RAW_SUMMARY_DATA);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -17,11 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices;
import android.app.Activity;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9PairingActivity;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
@ -65,7 +68,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
@Override
public GBDevice createDevice(GBDeviceCandidate candidate) {
return new GBDevice(candidate.getDevice().getAddress(), candidate.getName(), getDeviceType());
return new GBDevice(candidate.getDevice().getAddress(), candidate.getName(), null, getDeviceType());
}
@Override
@ -152,6 +155,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false;
}
@Override
public boolean supportsAlarmDescription(GBDevice device) {
return false;
}
@Override
public boolean supportsMusicInfo() {
return false;
@ -180,4 +188,10 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return null;
}
@Nullable
@Override
public Class<? extends Activity> getCalibrationActivity() {
return null;
}
}

View File

@ -128,6 +128,9 @@ public interface DeviceCoordinator {
@Nullable
Class<? extends Activity> getPairingActivity();
@Nullable
Class<? extends Activity> getCalibrationActivity();
/**
* Returns true if activity data fetching is supported by the device
* (with this coordinator).
@ -207,6 +210,12 @@ public interface DeviceCoordinator {
*/
boolean supportsAlarmSnoozing();
/**
* Returns true if this device/coordinator supports alarm descriptions
* @return
*/
boolean supportsAlarmDescription(GBDevice device);
/**
* Returns true if the given device supports heart rate measurements.
* @return

View File

@ -49,6 +49,7 @@ public class HuamiConst {
public static final String MI_BAND3_NAME = "Mi Band 3";
public static final String MI_BAND3_NAME_2 = "Xiaomi Band 3";
public static final String MI_BAND4_NAME = "Mi Smart Band 4";
public static final String MI_BAND5_NAME = "Mi Smart Band 5";
public static final String PREF_ACTIVATE_DISPLAY_ON_LIFT = "activate_display_on_lift_wrist";
public static final String PREF_DISPLAY_ON_LIFT_START = "display_on_lift_start";
@ -59,7 +60,7 @@ public class HuamiConst {
public static final String PREF_DISCONNECT_NOTIFICATION_END = "disconnect_notification_end";
public static final String PREF_DISPLAY_ITEMS = "display_items";
public static final String PREF_LANGUAGE = "language";
public static final String PREF_SHORTCUTS = "shortcuts";
public static final String PREF_EXPOSE_HR_THIRDPARTY = "expose_hr_thirdparty";
public static final String PREF_USE_CUSTOM_FONT = "use_custom_font";
@ -70,6 +71,16 @@ public class HuamiConst {
public static final String PREF_BUTTON_ACTION_BROADCAST_DELAY = "button_action_broadcast_delay";
public static final String PREF_BUTTON_ACTION_BROADCAST = "button_action_broadcast";
public static final String PREF_DEVICE_ACTION_SELECTION_OFF = "UNKNOWN";
public static final String PREF_DEVICE_ACTION_SELECTION_BROADCAST = "BROADCAST";
public static final String PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION = "events_forwarding_fellsleep_action_selection";
public static final String PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST = "prefs_events_forwarding_fellsleep_broadcast";
public static final String PREF_DEVICE_ACTION_WOKE_UP_SELECTION = "events_forwarding_wokeup_action_selection";
public static final String PREF_DEVICE_ACTION_WOKE_UP_BROADCAST = "prefs_events_forwarding_wokeup_broadcast";
public static final String PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION = "events_forwarding_startnonwear_action_selection";
public static final String PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST = "prefs_events_forwarding_startnonwear_broadcast";
public static int toActivityKind(int rawType) {
switch (rawType) {
case TYPE_DEEP_SLEEP:

View File

@ -35,7 +35,6 @@ import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -192,11 +191,6 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator {
return getTimePreference(HuamiConst.PREF_DISCONNECT_NOTIFICATION_END, "00:00", deviceAddress);
}
public static Set<String> getDisplayItems(String deviceAddress) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
return prefs.getStringSet(HuamiConst.PREF_DISPLAY_ITEMS, null);
}
public static boolean getUseCustomFont(String deviceAddress) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
return prefs.getBoolean(HuamiConst.PREF_USE_CUSTOM_FONT, false);

View File

@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
public abstract class HuamiFWHelper extends AbstractMiBandFWHelper {
protected HuamiFirmwareInfo firmwareInfo;
@ -110,6 +111,10 @@ public abstract class HuamiFWHelper extends AbstractMiBandFWHelper {
firmwareInfo.checkValid();
}
@Override
public HuamiFirmwareType getFirmwareType() {
return firmwareInfo.getFirmwareType();
}
public HuamiFirmwareInfo getFirmwareInfo() {
return firmwareInfo;
}

View File

@ -89,6 +89,7 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
R.xml.devicesettings_sync_calendar,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_buttonactions_with_longpress,
R.xml.devicesettings_device_actions,
R.xml.devicesettings_pairingkey
};
}

View File

@ -58,7 +58,8 @@ public class AmazfitBipSCoordinator extends HuamiCoordinator {
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
AmazfitBipSFWInstallHandler handler = new AmazfitBipSFWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
@ -76,18 +77,22 @@ public class AmazfitBipSCoordinator extends HuamiCoordinator {
return true;
}
@Override
public boolean supportsMusicInfo() {
return true;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitbip,
R.xml.devicesettings_amazfitbips,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_sync_calendar,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_buttonactions_with_longpress,
R.xml.devicesettings_high_mtu,
R.xml.devicesettings_pairingkey
};
}

View File

@ -0,0 +1,40 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbips;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbips.AmazfitBipSFirmwareInfo;
public class AmazfitBipSFWHelper extends HuamiFWHelper {
public AmazfitBipSFWHelper(Uri uri, Context context) throws IOException {
super(uri, context);
}
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = new AmazfitBipSFirmwareInfo(wholeFirmwareBytes);
if (!firmwareInfo.isHeaderValid()) {
throw new IllegalArgumentException("Not a an Amazfit Bip S firmware");
}
}
}

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbips;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
class AmazfitBipSFWInstallHandler extends AbstractMiBandFWInstallHandler {
AmazfitBipSFWInstallHandler(Uri uri, Context context) {
super(uri, context);
}
@Override
protected String getFwUpgradeNotice() {
return mContext.getString(R.string.fw_upgrade_notice_amazfitbip, helper.getHumanFirmwareVersion());
}
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new AmazfitBipSFWHelper(uri, context);
}
@Override
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.AMAZFITBIPS;
}
}

View File

@ -88,7 +88,7 @@ public class AmazfitGTRCoordinator extends HuamiCoordinator {
}
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitgtr,
R.xml.devicesettings_amazfitgtsgtr,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_liftwrist_display,

View File

@ -0,0 +1,102 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, Daniele Gobbetti, João
Paulo Barraca, José Rebelo, tiparega
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class AmazfitGTRLiteCoordinator extends HuamiCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTRLiteCoordinator.class);
@Override
public DeviceType getDeviceType() {
return DeviceType.AMAZFITGTR_LITE;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase("Amazfit GTR L")) {
return DeviceType.AMAZFITGTR_LITE;
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
AmazfitGTRLiteFWInstallHandler handler = new AmazfitGTRLiteFWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_REQUIRE_KEY;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public boolean supportsActivityTracks() {
return false;
}
@Override
public boolean supportsWeather() {
return true;
}
@Override
public boolean supportsMusicInfo() {
return true;
}
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitgtsgtr,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_sync_calendar,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}
}

View File

@ -0,0 +1,41 @@
/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgtr.AmazfitGTRLiteFirmwareInfo;
public class AmazfitGTRLiteFWHelper extends HuamiFWHelper {
public AmazfitGTRLiteFWHelper(Uri uri, Context context) throws IOException {
super(uri, context);
}
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = new AmazfitGTRLiteFirmwareInfo(wholeFirmwareBytes);
if (!firmwareInfo.isHeaderValid()) {
throw new IllegalArgumentException("Not a an Amazifit GTR Lite Firmware");
}
}
}

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
class AmazfitGTRLiteFWInstallHandler extends AbstractMiBandFWInstallHandler {
AmazfitGTRLiteFWInstallHandler(Uri uri, Context context) {
super(uri, context);
}
@Override
protected String getFwUpgradeNotice() {
return mContext.getString(R.string.fw_upgrade_notice_amazfitgtr, helper.getHumanFirmwareVersion());
}
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new AmazfitGTRLiteFWHelper(uri, context);
}
@Override
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.AMAZFITGTR_LITE;
}
}

View File

@ -88,7 +88,7 @@ public class AmazfitGTSCoordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitgtr,
R.xml.devicesettings_amazfitgtsgtr,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_liftwrist_display,

View File

@ -22,7 +22,6 @@ import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr.AmazfitGTRFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -40,7 +39,7 @@ class AmazfitGTSFWInstallHandler extends AbstractMiBandFWInstallHandler {
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new AmazfitGTRFWHelper(uri, context);
return new AmazfitGTSFWHelper(uri, context);
}
@Override

View File

@ -0,0 +1,101 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, Daniele Gobbetti, João
Paulo Barraca, José Rebelo, tiparega
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfittrex;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class AmazfitTRexCoordinator extends HuamiCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitTRexCoordinator.class);
@Override
public DeviceType getDeviceType() {
return DeviceType.AMAZFITTREX;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase("Amazfit T-Rex")) {
return DeviceType.AMAZFITTREX;
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
AmazfitTRexFWInstallHandler handler = new AmazfitTRexFWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_REQUIRE_KEY;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public boolean supportsActivityTracks() {
return true;
}
@Override
public boolean supportsWeather() {
return true;
}
@Override
public boolean supportsMusicInfo() {
return true;
}
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitgtsgtr,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_sync_calendar,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfittrex;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfittrex.AmazfitTRexFirmwareInfo;
public class AmazfitTRexFWHelper extends HuamiFWHelper {
public AmazfitTRexFWHelper(Uri uri, Context context) throws IOException {
super(uri, context);
}
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = new AmazfitTRexFirmwareInfo(wholeFirmwareBytes);
if (!firmwareInfo.isHeaderValid()) {
throw new IllegalArgumentException("Not a an Amazifit T-Rex firmware");
}
}
}

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfittrex;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
class AmazfitTRexFWInstallHandler extends AbstractMiBandFWInstallHandler {
AmazfitTRexFWInstallHandler(Uri uri, Context context) {
super(uri, context);
}
@Override
protected String getFwUpgradeNotice() {
return mContext.getString(R.string.fw_upgrade_notice_amazfit_trex, helper.getHumanFirmwareVersion());
}
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new AmazfitTRexFWHelper(uri, context);
}
@Override
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.AMAZFITTREX;
}
}

View File

@ -67,8 +67,19 @@ public class MiBand2FWInstallHandler extends AbstractMiBandFWInstallHandler {
Version v53 = MiBandConst.MI2_FW_VERSION_INTERMEDIATE_UPGRADE_53;
if (deviceVersion.compareTo(v53) < 0) {
String vInstall = getHelper().format(getHelper().getFirmwareVersion());
if (vInstall == null || new Version(vInstall).compareTo(v53) > 0) {
String newInfoText = getContext().getString(R.string.mi2_fw_installhandler_fw53_hint, v53.get()) + "\n\n" + installActivity.getInfoText();
try {
if (vInstall == null || new Version(vInstall).compareTo(v53) > 0) {
String newInfoText = getContext().getString(R.string.mi2_fw_installhandler_fw53_hint, v53.get()) +
"\n\n" +
installActivity.getInfoText();
installActivity.setInfoText(newInfoText);
}
} catch (IllegalArgumentException e) {
String newInfoText = getContext().getString(R.string.mi2_fw_installhandler_fw53_hint, v53.get()) +
"\n\n" +
installActivity.getInfoText() +
"\n\n" +
getContext().getString(R.string.error_version_check_extreme_caution, vInstall);
installActivity.setInfoText(newInfoText);
}
}

View File

@ -112,6 +112,7 @@ public class MiBand3Coordinator extends HuamiCoordinator {
R.xml.devicesettings_swipeunlock,
R.xml.devicesettings_sync_calendar,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_device_actions,
R.xml.devicesettings_pairingkey
};
}

View File

@ -0,0 +1,110 @@
/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, HardLight, José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.miband5;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class MiBand5Coordinator extends HuamiCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(MiBand5Coordinator.class);
@Override
public DeviceType getDeviceType() {
return DeviceType.MIBAND5;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase(HuamiConst.MI_BAND5_NAME)) {
return DeviceType.MIBAND5;
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
MiBand5FWInstallHandler handler = new MiBand5FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public boolean supportsWeather() {
return true;
}
@Override
public boolean supportsActivityTracks() {
return true;
}
@Override
public boolean supportsMusicInfo() {
return true;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_miband5,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_dateformat,
R.xml.devicesettings_nightmode,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_swipeunlock,
R.xml.devicesettings_sync_calendar,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey,
R.xml.devicesettings_high_mtu
};
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_REQUIRE_KEY;
}
}

View File

@ -0,0 +1,40 @@
/* Copyright (C) 2017-2020 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.miband5;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband5.MiBand5FirmwareInfo;
public class MiBand5FWHelper extends HuamiFWHelper {
public MiBand5FWHelper(Uri uri, Context context) throws IOException {
super(uri, context);
}
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = new MiBand5FirmwareInfo(wholeFirmwareBytes);
if (!firmwareInfo.isHeaderValid()) {
throw new IllegalArgumentException("Not a Mi Band 5 firmware");
}
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.miband5;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband5.MiBand5FWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
class MiBand5FWInstallHandler extends AbstractMiBandFWInstallHandler {
MiBand5FWInstallHandler(Uri uri, Context context) {
super(uri, context);
}
@Override
protected String getFwUpgradeNotice() {
return mContext.getString(R.string.fw_upgrade_notice_miband5, helper.getHumanFirmwareVersion());
}
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new MiBand5FWHelper(uri, context);
}
@Override
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.MIBAND5;
}
}

View File

@ -17,18 +17,23 @@
*/
package nodomain.freeyourgadget.gadgetbridge.devices.itag;
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 androidx.annotation.NonNull;
import java.util.Collection;
import java.util.Collections;
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.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -45,6 +50,27 @@ public class ITagCoordinator extends AbstractDeviceCoordinator {
return DeviceType.UNKNOWN;
}
@Override
public int getBondingStyle() {
// Some iTag devices do not support bonding but some do
return BONDING_STYLE_ASK;
}
@NonNull
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
ScanFilter filter = new ScanFilter.Builder()
.setDeviceName("iTag")
.setDeviceName("iTAG")
.setDeviceName("ITAG")
.setDeviceName("ITag")
.setDeviceName("Itag")
.setDeviceName("itag")
.build();
return Collections.singletonList(filter);
}
@Override
public DeviceType getDeviceType() {
return DeviceType.ITAG;

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.devices.lenovo;
public enum DataType {
STEPS(new byte[]{0x00, 0x00}),
SLEEP(new byte[]{0x00, 0x01}),
HEART_RATE(new byte[]{0x00, 0x02}),
BLOOD_PRESSURE(new byte[]{0x00, 0x06}),
INFRARED_TEMPERATURE(new byte[]{0x00, 0x08}),
ENVIRONMENT_TEMPERATURE(new byte[]{0x00, 0x09}),
AIR_PRESSURE(new byte[]{0x00, 0x0A});
private final byte[] value;
DataType(byte[] value) {
this.value = value;
}
public byte[] getValue() {
return value;
}
public static DataType getType(int value) {
for(DataType type : values()) {
int intVal = (type.getValue()[1] & 0xff) | ((type.getValue()[0] & 0xff) << 8);
if(intVal == value) {
return type;
}
}
throw new RuntimeException("No value defined for " + value);
}
}

View File

@ -0,0 +1,124 @@
/* Copyright (C) 2018-2019 Daniele Gobbetti, maxirnilian
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.lenovo;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.NumberPicker;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class LenovoWatchCalibrationActivity extends AbstractGBActivity {
private static final String STATE_DEVICE = "stateDevice";
private GBDevice device;
private NumberPicker pickerHour, pickerMinute, pickerSecond;
private Handler handler;
private Runnable holdCalibration;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_watchxplus_calibration);
pickerHour = findViewById(R.id.np_hour);
pickerMinute = findViewById(R.id.np_minute);
pickerSecond = findViewById(R.id.np_second);
pickerHour.setMinValue(1);
pickerHour.setMaxValue(12);
pickerHour.setValue(12);
pickerMinute.setMinValue(0);
pickerMinute.setMaxValue(59);
pickerMinute.setValue(0);
pickerSecond.setMinValue(0);
pickerSecond.setMaxValue(59);
pickerSecond.setValue(0);
handler = new Handler();
holdCalibration = new Runnable() {
@Override
public void run() {
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(LenovoWatchConstants.ACTION_CALIBRATION_HOLD));
handler.postDelayed(this, 10000);
}
};
Intent intent = getIntent();
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device == null && savedInstanceState != null) {
device = savedInstanceState.getParcelable(STATE_DEVICE);
}
if (device == null) {
finish();
}
final Button btCalibrate = findViewById(R.id.watch9_bt_calibrate);
btCalibrate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btCalibrate.setEnabled(false);
handler.removeCallbacks(holdCalibration);
Intent calibrationData = new Intent(LenovoWatchConstants.ACTION_CALIBRATION_SEND);
calibrationData.putExtra(LenovoWatchConstants.VALUE_CALIBRATION_HOUR, pickerHour.getValue());
calibrationData.putExtra(LenovoWatchConstants.VALUE_CALIBRATION_MINUTE, pickerMinute.getValue());
calibrationData.putExtra(LenovoWatchConstants.VALUE_CALIBRATION_SECOND, pickerSecond.getValue());
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibrationData);
finish();
}
});
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(STATE_DEVICE, device);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
device = savedInstanceState.getParcelable(STATE_DEVICE);
}
@Override
protected void onStart() {
super.onStart();
Intent calibration = new Intent(LenovoWatchConstants.ACTION_CALIBRATION);
calibration.putExtra(LenovoWatchConstants.ACTION_ENABLE, true);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
handler.postDelayed(holdCalibration, 1000);
}
@Override
protected void onStop() {
super.onStop();
Intent calibration = new Intent(LenovoWatchConstants.ACTION_CALIBRATION);
calibration.putExtra(LenovoWatchConstants.ACTION_ENABLE, false);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
handler.removeCallbacks(holdCalibration);
}
}

View File

@ -0,0 +1,79 @@
/* Copyright (C) 2018-2019 maxirnilian
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.lenovo;
public class LenovoWatchConstants {
public static final byte RESPONSE = 0x13;
public static final byte REQUEST = 0x31;
public static final byte WRITE_VALUE = 0x01;
public static final byte READ_VALUE = 0x02;
public static final byte TASK = 0x04;
public static final byte KEEP_ALIVE = -0x80;
public static final byte[] CMD_HEADER = new byte[]{0x23, 0x01, 0x00, 0x00, 0x00};
// byte[] COMMAND = new byte[]{0x23, 0x01, 0x00, 0x31, 0x00, ... , 0x00}
// | | | | | | Checksum
// | | | | | Command + value
// | | | | Sequence number
// | | | Response/Request indicator
// | | Value length
// | |
// ----- Header
public static final byte[] CMD_FIRMWARE_INFO = new byte[]{0x01, 0x02};
public static final byte[] CMD_AUTHORIZATION_TASK = new byte[]{0x01, 0x05};
public static final byte[] CMD_TIME_SETTINGS = new byte[]{0x01, 0x08};
public static final byte[] CMD_ALARM_SETTINGS = new byte[]{0x01, 0x0A};
public static final byte[] CMD_BATTERY_INFO = new byte[]{0x01, 0x14};
public static final byte[] CMD_NOTIFICATION_TASK = new byte[]{0x03, 0x01};
public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02};
public static final byte[] CMD_CALIBRATION_INIT_TASK = new byte[]{0x03, 0x31};
public static final byte[] CMD_CALIBRATION_TASK = new byte[]{0x03, 0x33, 0x01};
public static final byte[] CMD_CALIBRATION_KEEP_ALIVE = new byte[]{0x03, 0x34};
public static final byte[] CMD_DO_NOT_DISTURB_SETTINGS = new byte[]{0x03, 0x61};
public static final byte[] CMD_FITNESS_GOAL_SETTINGS = new byte[]{0x10, 0x02};
public static final byte[] RESP_AUTHORIZATION_TASK = new byte[]{0x01, 0x01, 0x05};
public static final byte[] RESP_BUTTON_INDICATOR = new byte[]{0x04, 0x03, 0x11};
public static final byte[] RESP_ALARM_INDICATOR = new byte[]{-0x80, 0x01, 0x0A};
public static final byte[] RESP_FIRMWARE_INFO = new byte[]{0x08, 0x01, 0x02};
public static final byte[] RESP_TIME_SETTINGS = new byte[]{0x08, 0x01, 0x08};
public static final byte[] RESP_BATTERY_INFO = new byte[]{0x08, 0x01, 0x14};
public static final byte[] RESP_NOTIFICATION_SETTINGS = new byte[]{0x01, 0x03, 0x02};
public static final String ACTION_ENABLE = "action.watch9.enable";
public static final String ACTION_CALIBRATION
= "nodomain.freeyourgadget.gadgetbridge.devices.action.lenovowatch.start_calibration";
public static final String ACTION_CALIBRATION_SEND
= "nodomain.freeyourgadget.gadgetbridge.devices.action.lenovowatch.send_calibration";
public static final String ACTION_CALIBRATION_HOLD
= "nodomain.freeyourgadget.gadgetbridge.devices.action.lenovowatch.keep_calibrating";
public static final String VALUE_CALIBRATION_HOUR
= "value.lenovowatch.calibration_hour";
public static final String VALUE_CALIBRATION_MINUTE
= "value.lenovowatch.calibration_minute";
public static final String VALUE_CALIBRATION_SECOND
= "value.lenovowatch.calibration_second";
}

View File

@ -0,0 +1,130 @@
/* Copyright (C) 2018-2019 Daniele Gobbetti, maxirnilian
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.lenovo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
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;
public class LenovoWatchPairingActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(LenovoWatchPairingActivity.class);
private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate";
private TextView message;
private GBDeviceCandidate deviceCandidate;
private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug("pairing activity: device changed: " + device);
if (deviceCandidate.getMacAddress().equals(device.getAddress())) {
if (device.isInitialized()) {
pairingFinished();
} else if (device.isConnecting() || device.isInitializing()) {
LOG.info("still connecting/initializing device...");
}
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_watch9_pairing);
message = findViewById(R.id.watch9_pair_message);
Intent intent = getIntent();
deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
if (deviceCandidate == null && savedInstanceState != null) {
deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
}
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();
return;
}
startPairing();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(STATE_DEVICE_CANDIDATE, deviceCandidate);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
}
@Override
protected void onDestroy() {
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
super.onDestroy();
}
private void startPairing() {
message.setText(getString(R.string.pairing, deviceCandidate));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
GBApplication.deviceService().disconnect();
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);
}
}
private void pairingFinished() {
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
}

View File

@ -0,0 +1,115 @@
/* Copyright (C) 2018-2019 maxirnilian
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.LenovoWatchConstants;
public final class WatchXPlusConstants extends LenovoWatchConstants {
public static final UUID UUID_SERVICE_WATCHXPLUS = UUID.fromString("0000a800-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_UNKNOWN_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_WRITE = UUID.fromString("0000a801-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_DATABASE_READ = UUID.fromString("0000a802-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_3 = UUID.fromString("0000a803-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_4 = UUID.fromString("0000a804-0000-1000-8000-00805f9b34fb");
public static final String PREF_FIND_PHONE = "prefs_find_phone";
public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration";
// new
public static final String PREF_CONTINIOUS_RING = "notification_enable_continious_ring";
public static final String PREF_REPEAT_RING = "notification_repeat_ring";
public static final String PREF_MISSED_CALL_ENABLE = "notification_enable_missed_call";
public static final String PREF_MISSED_CALL_REPEAT = "notification_repeat_missed_call";
public static final String PREF_BUTTON_REJECT = "notification_button_reject";
public static final String PREF_SHAKE_REJECT = "notification_shake_reject";
public static final String PREF_FORCE_TIME = "pref_device_spec_settings_force_time";
public static final String PREF_BP_CAL_LOW = "pref_sensors_bp_calibration_low";
public static final String PREF_BP_CAL_HIGH = "pref_sensors_bp_calibration_high";
public static final String PREF_DO_NOT_DISTURB = "do_not_disturb_no_auto";
public static final String PREF_DO_NOT_DISTURB_START = "do_not_disturb_no_auto_start";
public static final String PREF_DO_NOT_DISTURB_END = "do_not_disturb_no_auto_end";
public static final String PREF_LONGSIT_START = "pref_longsit_start";
public static final String PREF_LONGSIT_END = "pref_longsit_end";
public static final String PREF_SHOW_RAW_GRAPH = "show_raw_graph";
// moved to gear icon (per device settings)
public static final String PREF_LANGUAGE = "language";
// time format constants
public static final byte ARG_SET_TIMEMODE_24H = 0x00;
public static final byte ARG_SET_TIMEMODE_12H = 0x01;
public static final int NOTIFICATION_CHANNEL_DEFAULT = 0;
public static final int NOTIFICATION_CHANNEL_PHONE_CALL = 10;
public static final byte[] CMD_WEATHER_SET = new byte[]{0x01, 0x10};
public static final byte[] CMD_RETRIEVE_DATA_COUNT = new byte[]{(byte)0xF0, 0x10};
public static final byte[] CMD_RETRIEVE_DATA_DETAILS = new byte[]{(byte)0xF0, 0x11};
public static final byte[] CMD_RETRIEVE_DATA_CONTENT = new byte[]{(byte)0xF0, 0x12};
public static final byte[] CMD_REMOVE_DATA_CONTENT = new byte[]{(byte)0xF0, 0x32};
public static final byte[] CMD_BLOOD_PRESSURE_MEASURE = new byte[]{0x05, 0x0D};
public static final byte[] CMD_HEART_RATE_MEASURE = new byte[]{0x03, 0x23};
public static final byte[] CMD_IS_BP_CALIBRATED = new byte[]{0x05, 0x0B};
public static final byte[] CMD_BP_CALIBRATION = new byte[]{0x05, 0x0C};
public static final byte[] CMD_NOTIFICATION_TEXT_TASK = new byte[]{0x03, 0x06};
public static final byte[] CMD_NOTIFICATION_CANCEL = new byte[]{0x03, 0x04};
public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02};
public static final byte[] CMD_POWER_MODE = new byte[]{0x03, -0x7F};
public static final byte[] CMD_SET_DND_HOURS_TIME = new byte[]{0x03, 0x62};
public static final byte[] CMD_SET_DND_HOURS_SWITCH = new byte[]{0x03, 0x61};
public static final byte[] CMD_SET_PERSONAL_INFO = new byte[]{0x01, 0x0E};
public static final byte[] CMD_INACTIVITY_REMINDER_SWITCH = new byte[]{0x03, 0x51};
public static final byte[] CMD_INACTIVITY_REMINDER_SET = new byte[]{0x03, 0x52};
public static final byte[] CMD_SET_UNITS = new byte[]{0x03, -0x6D};
public static final byte[] CMD_FITNESS_GOAL_SETTINGS = new byte[]{0x10, 0x02};
public static final byte[] CMD_DAY_STEPS_INFO = new byte[]{0x10, 0x03};
public static final byte[] CMD_SHAKE_SWITCH = new byte[]{0x03, -0x6E};
public static final byte[] CMD_DISCONNECT_REMIND = new byte[]{0x00, 0x11};
public static final byte[] CMD_TIME_LANGUAGE = new byte[]{0x03, -0x6F};
public static final byte[] CMD_ALTITUDE = new byte[]{0x05, 0x0A};
public static final byte[] RESP_SHAKE_SWITCH = new byte[]{0x08, 0x03, -0x6E};
public static final byte[] RESP_DISCONNECT_REMIND = new byte[]{0x08, 0x00, 0x11};
public static final byte[] RESP_IS_BP_CALIBRATED = new byte[]{0x08, 0x05, 0x0B};
public static final byte[] RESP_BUTTON_WHILE_RING = new byte[]{0x04, 0x03, 0x03};
public static final byte[] RESP_BP_CALIBRATION = new byte[]{0x08, 0x05, 0x0C};
public static final byte[] RESP_SET_PERSONAL_INFO = new byte[]{0x08, 0x01, 0x0E};
public static final byte[] RESP_GOAL_AIM_STATUS = new byte[]{0x08, 0x10, 0x02};
public static final byte[] RESP_INACTIVITY_REMINDER_SWITCH = new byte[]{0x08, 0x03, 0x51};
public static final byte[] RESP_INACTIVITY_REMINDER_SET = new byte[]{0x08, 0x03, 0x52};
public static final byte[] RESP_AUTHORIZATION_TASK = new byte[]{0x01, 0x01, 0x05};
public static final byte[] RESP_DAY_STEPS_INDICATOR = new byte[]{0x08, 0x10, 0x03};
public static final byte[] RESP_HEARTRATE = new byte[]{(byte) 0x80, 0x15, 0x03};
public static final byte[] RESP_DATA_COUNT = new byte[]{0x08, (byte)0xF0, 0x10};
public static final byte[] RESP_DATA_DETAILS = new byte[]{0x08, (byte)0xF0, 0x11};
public static final byte[] RESP_DATA_CONTENT = new byte[]{0x08, (byte)0xF0, 0x12};
public static final byte[] RESP_DATA_CONTENT_REMOVE = new byte[]{-0x80, (byte)0xF0, 0x32};
public static final byte[] RESP_BP_MEASURE_STARTED = new byte[]{0x08, 0x05, 0x0D};
}

View File

@ -0,0 +1,283 @@
package nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus;
import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.LenovoWatchCalibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.LenovoWatchPairingActivity;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.lenovo.watchxplus.WatchXPlusDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusDeviceSupport.class);
private static final int FindPhone_ON = -1;
public static final int FindPhone_OFF = 0;
public static boolean isBPCalibrated = false;
private static final Prefs prefs = GBApplication.getPrefs();
@NonNull
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
ParcelUuid watchXpService = new ParcelUuid(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(watchXpService).build();
return Collections.singletonList(filter);
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String macAddress = candidate.getMacAddress().toUpperCase();
String deviceName = candidate.getName().toUpperCase();
if (candidate.supportsService(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS)) {
return DeviceType.WATCHXPLUS;
} else if (macAddress.startsWith("DC:41:E5")) {
return DeviceType.WATCHXPLUS;
} else if (deviceName.equalsIgnoreCase("WATCH XPLUS")) {
return DeviceType.WATCHXPLUS;
// add initial support for Watch X non-plus (forces Watch X to be recognized as Watch XPlus)
// Watch X non-plus have same MAC address as Watch 9 (starts with "1C:87:79")
} else if (deviceName.equalsIgnoreCase("WATCH X")) {
return DeviceType.WATCHXPLUS;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.WATCHXPLUS;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
return LenovoWatchPairingActivity.class;
}
@Override
public boolean supportsActivityDataFetching() {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new WatchXPlusSampleProvider(device, session);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public int getAlarmSlotCount() {
return 3;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public String getManufacturer() {
return "Lenovo";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() { return false; }
@Override
public boolean supportsWeather() {
return true;
}
@Override
public boolean supportsFindDevice() { return false; }
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_liftwrist_display_noshed,
R.xml.devicesettings_disconnectnotification_noshed,
R.xml.devicesettings_donotdisturb_no_auto,
R.xml.devicesettings_longsit,
R.xml.devicesettings_find_phone,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_power_mode,
R.xml.devicesettings_watchxplus
};
}
// find phone settings
/**
* @return {@link #FindPhone_OFF}, {@link #FindPhone_ON}, or the duration
*/
public static int getFindPhone(SharedPreferences sharedPrefs) {
String findPhone = sharedPrefs.getString(WatchXPlusConstants.PREF_FIND_PHONE, getContext().getString(R.string.p_off));
assert findPhone != null;
if (findPhone.equals(getContext().getString(R.string.p_off))) {
return FindPhone_OFF;
} else if (findPhone.equals(getContext().getString(R.string.p_on))) {
return FindPhone_ON;
} else { // Duration
String duration = sharedPrefs.getString(WatchXPlusConstants.PREF_FIND_PHONE_DURATION, "0");
try {
int iDuration;
try {
assert duration != null;
iDuration = Integer.valueOf(duration);
} catch (Exception ex) {
iDuration = 60;
}
return iDuration;
} catch (Exception e) {
return FindPhone_ON;
}
}
}
/**
* @param startOut out Only hour/minute are used.
* @param endOut out Only hour/minute are used.
* @return True if DND hours are enabled.
*/
public static boolean getDNDHours(String deviceAddress, Calendar startOut, Calendar endOut) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
String doNotDisturb = prefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB, getContext().getString(R.string.p_off));
assert doNotDisturb != null;
if (doNotDisturb.equals(getContext().getString(R.string.p_off))) {
LOG.info(" DND is disabled ");
return false;
} else {
String start = prefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB_START, "01:00");
String end = prefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB_END, "06:00");
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startOut.setTime(df.parse(start));
endOut.setTime(df.parse(end));
return true;
} catch (Exception e) {
return false;
}
}
}
/**
* @param startOut out Only hour/minute are used.
* @param endOut out Only hour/minute are used.
* @return True if DND hours are enabled.
*/
public static boolean getLongSitHours(String deviceAddress, Calendar startOut, Calendar endOut) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH, false);
if (!enabled) {
LOG.info(" Long sit reminder is disabled ");
return false;
} else {
String start = prefs.getString(WatchXPlusConstants.PREF_LONGSIT_START, "06:00");
String end = prefs.getString(WatchXPlusConstants.PREF_LONGSIT_END, "23:00");
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startOut.setTime(df.parse(start));
endOut.setTime(df.parse(end));
return true;
} catch (Exception e) {
return false;
}
}
}
@Nullable
@Override
public Class<? extends Activity> getCalibrationActivity() {
return LenovoWatchCalibrationActivity.class;
}
}

View File

@ -0,0 +1,524 @@
package nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class WatchXPlusSampleProvider extends AbstractSampleProvider<WatchXPlusActivitySample> {
private GBDevice mDevice;
private DaoSession mSession;
private final float movementDivisor = 950.0f;
private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusSampleProvider.class);
public WatchXPlusSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
mSession = session;
mDevice = device;
}
@Override
public int normalizeType(int rawType) {
return rawType;
}
@Override
public int toRawActivityKind(int activityKind) {
LOG.info(" toRawActivityKind: " + activityKind);
return activityKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
float newIntensity = 0;
if (rawIntensity <= 0) {
//newIntensity = (rawIntensity * 0.5f) + 0.7f;
newIntensity = rawIntensity;
} else {
newIntensity = rawIntensity / movementDivisor;
}
//LOG.info(" normalizeIntensity: " + rawIntensity + " to " + newIntensity);
return newIntensity;
//return rawIntensity;
}
@Override
public WatchXPlusActivitySample createActivitySample() {
return new WatchXPlusActivitySample();
}
@Override
public AbstractDao<WatchXPlusActivitySample, ?> getSampleDao() {
return getSession().getWatchXPlusActivitySampleDao();
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return WatchXPlusActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return WatchXPlusActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return WatchXPlusActivitySampleDao.Properties.DeviceId;
}
// generate ActivityKind.TYPE_NOT_MEASURED if there are no data for more than 15 min. and less than 60 min.
// generate ActivityKind.TYPE_NOT_WORN if there are no data for more than 60 min.
@NonNull
private List<WatchXPlusActivitySample> checkActivityData(List<WatchXPlusActivitySample> samples, int notMeasuredTS, int notWornTS) {
int oldTS = 0;
int newTS = 0;
oldTS = samples.get(0).getTimestamp();
for (int i = 0; i < samples.size(); i++) {
//oldTS = resultList.get(i).getTimestamp();
newTS = samples.get(i).getTimestamp();
if ((newTS - oldTS) < notMeasuredTS) { //check data timestamp diff is more than 15 min
oldTS = samples.get(i).getTimestamp();
} else if (((newTS - oldTS) > notMeasuredTS) && ((newTS - oldTS) < notWornTS)) { //set data to ActivityKind.TYPE_NOT_MEASURED) if timestamp diff is more than 15 min
samples.get(i-1).setRawKind(ActivityKind.TYPE_NOT_MEASURED);
samples.get(i).setRawKind(ActivityKind.TYPE_NOT_MEASURED);
oldTS = samples.get(i).getTimestamp();
} else if ((newTS - oldTS) > notWornTS) { //set data to ActivityKind.TYPE_NOT_WORN if timestamp diff is more than 60 min
samples.get(i-1).setRawKind(ActivityKind.TYPE_NOT_WORN);
samples.get(i).setRawKind(ActivityKind.TYPE_NOT_WORN);
oldTS = samples.get(i).getTimestamp();
}
}
return samples;
}
@Override
public List<WatchXPlusActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to) {
boolean showRawData = GBApplication.getDeviceSpecificSharedPrefs(mDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_SHOW_RAW_GRAPH, false);
if (showRawData) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
}
List<WatchXPlusActivitySample> samples = getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
int numEntries = samples.size();
if (numEntries < 3) {
return samples;
}
List<WatchXPlusActivitySample> resultList = new ArrayList<>(numEntries);
// how many elements to scan for sleep sate before and after sleep block
int seekAhead = 10;
boolean secondBlock = false;
// find sleep start and sleep stop index based on ActivityKind.TYPE_DEEP_SLEEP BLOCK 1
int sleepStartIndex_1 = 0;
int sleepStopIndex_1 = numEntries;
int countNextSleepStart_1 = 0;
int countNextSleepStop_1 = 0;
for (int i = 0; i < numEntries; i++) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_DEEP_SLEEP) {
// normalize RawIntensity
samples.get(i).setRawIntensity(1000);
// find sleep start index
if (sleepStartIndex_1 == 0) {
sleepStartIndex_1 = i;
sleepStopIndex_1 = sleepStartIndex_1;
countNextSleepStop_1 = sleepStopIndex_1;
} else {
if (countNextSleepStart_1 == 0) {
countNextSleepStart_1 = i;
// reset start index if next index is far ahead
if ((countNextSleepStart_1 - sleepStartIndex_1) > seekAhead * 3) {
sleepStartIndex_1 = countNextSleepStart_1;
sleepStopIndex_1 = sleepStartIndex_1;
countNextSleepStop_1 = sleepStopIndex_1;
}
}
}
if ((i - sleepStopIndex_1) < (seekAhead * 4)) {
sleepStopIndex_1 = i;
}
countNextSleepStop_1 = i;
}
}
// find sleep start and sleep stop index based on ActivityKind.TYPE_DEEP_SLEEP BLOCK 2
int sleepStartIndex_2 = 0;
int sleepStopIndex_2 = numEntries;
int countNextSleepStart_2 = 0;
int countNextSleepStop_2 = 0;
int next_block = numEntries;
for (int i = sleepStopIndex_1 + 1; i < numEntries; i++) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_DEEP_SLEEP) {
// find sleep start index
if (sleepStartIndex_2 == 0) {
sleepStartIndex_2 = i;
sleepStopIndex_2 = sleepStartIndex_2;
countNextSleepStop_2 = sleepStopIndex_2;
} else {
if (countNextSleepStart_2 == 0) {
countNextSleepStart_2 = i;
// reset start index if next index is far ahead
if ((countNextSleepStart_2 - sleepStartIndex_2) > seekAhead * 3) {
sleepStartIndex_2 = countNextSleepStart_2;
sleepStopIndex_2 = sleepStartIndex_2;
countNextSleepStop_2 = sleepStopIndex_2;
}
}
}
if ((i - sleepStopIndex_2) < (seekAhead * 4)) {
sleepStopIndex_2 = i;
}
countNextSleepStop_2 = i;
}
}
if (sleepStartIndex_2 != 0) {
secondBlock = true;
LOG.info(" Found second block ");
}
LOG.info(" sleep_1 begin index:" + sleepStartIndex_1 + " next index: " + countNextSleepStart_1 + " sleep end index: " + sleepStopIndex_1 + " sleep end: " + countNextSleepStop_1);
if (secondBlock) {
LOG.info(" sleep_2 begin index:" + sleepStartIndex_2 + " next index: " + countNextSleepStart_2 + " sleep end index: " + sleepStopIndex_2 + " sleep end: " + countNextSleepStop_2);
}
// SLEEP BLOCK 1
// add all activity before sleep start
if (secondBlock) {
next_block = sleepStartIndex_2;
}
int newSleepStartIndex_1 = 0;
if (sleepStartIndex_1 >= seekAhead) {
newSleepStartIndex_1 = sleepStartIndex_1 - seekAhead;
} else {
newSleepStartIndex_1 = 0;
}
for (int i = 0; i < newSleepStartIndex_1; i++) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
if (samples.get(i).getRawIntensity() <= 300) {
samples.get(i).setRawIntensity(200);
} else if ((samples.get(i).getRawIntensity() <= 1000) && (samples.get(i).getRawIntensity() > 100)) {
samples.get(i).setRawIntensity(400);
} if (samples.get(i).getRawIntensity() > 1000) {
samples.get(i).setRawIntensity(600);
}
samples.get(i).setRawKind(1);
resultList.add(samples.get(i));
} else {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_ACTIVITY) {
if (i < (newSleepStartIndex_1 - 3)) {
if ((samples.get(i + 1).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) || (samples.get(i + 2).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) || (samples.get(i + 3).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP)) {
samples.get(i).setRawKind(1);
//samples.get(i).setRawIntensity(700);
} else {
samples.get(i).setRawIntensity(1000);
}
}
//samples.get(i).setRawIntensity(1000);
} else {
samples.get(i).setRawIntensity(1000);
}
resultList.add(samples.get(i));
}
}
// add sleep activity
int newSleepStopIndex_1;
if ((sleepStopIndex_1 + seekAhead * 2) < next_block) {
newSleepStopIndex_1 = sleepStopIndex_1 + seekAhead * 2;
} else {
newSleepStopIndex_1 = next_block;
}
boolean replaceActivity_1 = false;
for (int i = newSleepStartIndex_1; i < newSleepStopIndex_1; i++) {
ActivitySample sample = samples.get(i);
if (i < sleepStartIndex_1) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
replaceActivity_1 = true;
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
} else {
if (replaceActivity_1) {
samples.get(i).setRawKind(2);
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
} else {
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
}
}
}
if ((samples.get(i).getRawKind() == ActivityKind.TYPE_DEEP_SLEEP) || (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP)) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
if (i > 0) {
if (samples.get(i - 1).getHeartRate() > 0) {
samples.get(i).setHeartRate(samples.get(i - 1).getHeartRate());
}
} else {
if (samples.get(i + 1).getHeartRate() > 0) {
samples.get(i).setHeartRate(samples.get(i + 1).getHeartRate());
}
}
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
} else {
samples.get(i).setRawIntensity(1000);
resultList.add(samples.get(i));
}
}
if ((samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) && (i > sleepStopIndex_1)) {
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
}
}
// add remaining activity
if (newSleepStopIndex_1 < next_block) {
for (int i = newSleepStopIndex_1; i < (next_block-1); i++) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
if (samples.get(i).getRawIntensity() <= 300) {
samples.get(i).setRawIntensity(200);
} else if ((samples.get(i).getRawIntensity() <= 1000) && (samples.get(i).getRawIntensity() > 100)) {
samples.get(i).setRawIntensity(400);
} if (samples.get(i).getRawIntensity() > 1000) {
samples.get(i).setRawIntensity(600);
}
samples.get(i).setRawKind(1);
resultList.add(samples.get(i));
} else {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_ACTIVITY) {
if (i < (next_block - 3)) {
if ((samples.get(i + 1).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) || (samples.get(i + 2).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) || (samples.get(i + 3).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP)) {
samples.get(i).setRawKind(1);
//samples.get(i).setRawIntensity(700);
} else {
samples.get(i).setRawIntensity(1000);
}
}
//samples.get(i).setRawIntensity(1000);
} else {
samples.get(i).setRawIntensity(1000);
}
resultList.add(samples.get(i));
}
}
}
// SLEEP BLOCK 2
if (secondBlock) {
// add sleep activity
int newSleepStopIndex_2;
int newSleepStartIndex_2 = 0;
boolean replaceActivity_2 = false;
if (sleepStartIndex_2 >= next_block + seekAhead) {
newSleepStartIndex_2 = sleepStartIndex_2 - seekAhead;
} else {
newSleepStartIndex_2 = next_block;
}
if ((sleepStopIndex_2 + seekAhead * 2) < numEntries) {
newSleepStopIndex_2 = sleepStopIndex_2 + seekAhead * 2;
} else {
newSleepStopIndex_2 = numEntries;
}
for (int i = newSleepStartIndex_2; i < newSleepStopIndex_2; i++) {
ActivitySample sample = samples.get(i);
if (i < sleepStartIndex_2) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
replaceActivity_2 = true;
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
} else {
if (replaceActivity_2) {
samples.get(i).setRawKind(2);
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
} else {
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
}
}
}
if ((samples.get(i).getRawKind() == ActivityKind.TYPE_DEEP_SLEEP) || (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP)) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
if (i > 0) {
if (samples.get(i - 1).getHeartRate() > 0) {
samples.get(i).setHeartRate(samples.get(i - 1).getHeartRate());
}
} else {
if (samples.get(i + 1).getHeartRate() > 0) {
samples.get(i).setHeartRate(samples.get(i + 1).getHeartRate());
}
}
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
} else {
samples.get(i).setRawIntensity(1000);
resultList.add(samples.get(i));
}
}
if ((samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) && (i > sleepStopIndex_2)) {
samples.get(i).setRawIntensity(600);
resultList.add(samples.get(i));
}
}
// add remaining activity
if (newSleepStopIndex_2 < numEntries) {
for (int i = newSleepStopIndex_2; i < (numEntries - 1); i++) {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
if (samples.get(i).getRawIntensity() <= 300) {
samples.get(i).setRawIntensity(200);
} else if ((samples.get(i).getRawIntensity() <= 1000) && (samples.get(i).getRawIntensity() > 100)) {
samples.get(i).setRawIntensity(400);
}
if (samples.get(i).getRawIntensity() > 1000) {
samples.get(i).setRawIntensity(600);
}
samples.get(i).setRawKind(1);
resultList.add(samples.get(i));
} else {
if (samples.get(i).getRawKind() == ActivityKind.TYPE_ACTIVITY) {
if (i < (numEntries - 3)) {
if ((samples.get(i + 1).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) || (samples.get(i + 2).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP) || (samples.get(i + 3).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP)) {
samples.get(i).setRawKind(1);
//samples.get(i).setRawIntensity(700);
} else {
samples.get(i).setRawIntensity(1000);
}
}
//samples.get(i).setRawIntensity(1000);
} else {
samples.get(i).setRawIntensity(1000);
}
resultList.add(samples.get(i));
}
}
}
}
// add one ActivityKind.TYPE_NOT_MEASURED at end of data
samples.get(numEntries-1).setRawIntensity(0);
samples.get(numEntries-1).setRawKind(ActivityKind.TYPE_NOT_MEASURED);
samples.get(numEntries-1).setHeartRate(0);
resultList.add(samples.get(numEntries-1));
// find all steps, total activity intensity and maxHR
int totalSteps = 0;
int maxHeartRate = 10;
numEntries = resultList.size();
for (int i = 0; i < numEntries-1; i++) {
if (resultList.get(i).getRawKind() == ActivityKind.TYPE_ACTIVITY) {
if (resultList.get(i).getSteps() > 0) {
totalSteps = totalSteps + resultList.get(i).getSteps();
}
}
if (resultList.get(i).getHeartRate() > maxHeartRate) {
maxHeartRate = resultList.get(i).getHeartRate();
}
}
// reformat activity data based on heart rate
int newIntensity, correctedSteps;
int totalIntensity = 0;
for (int i = 0; i < numEntries-1; i++) {
if ((resultList.get(i).getRawKind() == ActivityKind.TYPE_ACTIVITY) || (resultList.get(i).getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP)) {
if (resultList.get(i).getRawIntensity() <= 600) { // set interpolated intensity based on heart rate for every TYPE_ACTIVITY which are converted from TYPE_LIGHT_SLEEP
if (resultList.get(i).getHeartRate() < 10) {
newIntensity = resultList.get(i).getRawIntensity() + ((maxHeartRate - resultList.get(i+1).getHeartRate()) * 2);
} else {
newIntensity = resultList.get(i).getRawIntensity() + ((maxHeartRate - resultList.get(i).getHeartRate()) * 2);
}
} else { // because there are not RAW intensity values for every TYPE_ACTIVITY set interpolated intensity based on heart rate
newIntensity = resultList.get(i).getRawIntensity() - ((maxHeartRate - resultList.get(i).getHeartRate()) * 2);
}
/*
if (stepsPerActivity > 0.0f) { // because there are not steps values for every TYPE_ACTIVITY set interpolated steps
correctedSteps = (int) (resultList.get(i).getRawIntensity() / stepsPerActivity);
resultList.get(i).setSteps(correctedSteps);
}
*/
resultList.get(i).setRawIntensity(newIntensity);
if (resultList.get(i).getRawIntensity() > 0) {
totalIntensity = totalIntensity + newIntensity;
}
} else { // because there are not TYPE_DEEP_SLEEP intensity set random DEEP_SLEEP intensity
Random r = new Random();
newIntensity = resultList.get(i).getRawIntensity() - ((maxHeartRate - (int)(r.nextFloat() * maxHeartRate)) * 2);
resultList.get(i).setRawIntensity(newIntensity);
if (resultList.get(i).getRawIntensity() > 0) {
totalIntensity = totalIntensity + newIntensity;
}
}
}
// because there are not steps values for every TYPE_ACTIVITY set interpolated steps
float stepsPerActivity = 0.000f;
int newTotalSteps = 0;
int activityCount = 0;
if (totalSteps > 0) {
stepsPerActivity = totalIntensity / totalSteps;
for (int i = 0; i < numEntries - 1; i++) {
if (resultList.get(i).getRawKind() == ActivityKind.TYPE_ACTIVITY) {
if (stepsPerActivity > 0.0f) {
correctedSteps = (int) (resultList.get(i).getRawIntensity() / stepsPerActivity);
resultList.get(i).setSteps(correctedSteps);
newTotalSteps = newTotalSteps + correctedSteps;
activityCount = activityCount + 1;
}
}
}
}
if (newTotalSteps < totalSteps) {
int stepsDiff = newTotalSteps - totalSteps;
int increaseStepsWith = stepsDiff / activityCount;
if (increaseStepsWith <= 1) {
increaseStepsWith = 2;
}
newTotalSteps = 0;
for (int i = 0; i < numEntries - 1; i++) {
if (resultList.get(i).getRawKind() == ActivityKind.TYPE_ACTIVITY) {
correctedSteps = resultList.get(i).getSteps() + increaseStepsWith;
newTotalSteps = newTotalSteps + correctedSteps;
if (newTotalSteps <= totalSteps) {
resultList.get(i).setSteps(correctedSteps);
} else {
break;
}
}
}
}
return checkActivityData(resultList, 900, 3600);
}
}

View File

@ -20,17 +20,20 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
@ -73,10 +76,12 @@ public abstract class AbstractMiBandFWHelper {
public abstract int getFirmware2Version();
public static String formatFirmwareVersion(int version) {
if (version == -1)
if (version == -1) {
return GBApplication.getContext().getString(R.string._unknown_);
}
return String.format("%d.%d.%d.%d",
return String.format(Locale.UK,
"%d.%d.%d.%d",
version >> 24 & 255,
version >> 16 & 255,
version >> 8 & 255,
@ -122,4 +127,6 @@ public abstract class AbstractMiBandFWHelper {
protected abstract void determineFirmwareInfo(byte[] wholeFirmwareBytes);
public abstract void checkValid() throws IllegalArgumentException;
public abstract HuamiFirmwareType getFirmwareType();
}

View File

@ -30,6 +30,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType.WATCHFACE;
public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWInstallHandler.class);
@ -58,7 +60,7 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
protected abstract AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException;
protected GenericItem createInstallItem(GBDevice device) {
private GenericItem createInstallItem(GBDevice device) {
return new GenericItem(mContext.getString(R.string.installhandler_firmware_name, mContext.getString(device.getType().getName()), helper.getFirmwareKind(), helper.getHumanFirmwareVersion()));
}
@ -98,23 +100,25 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
return;
}
StringBuilder builder = new StringBuilder();
if (helper.isSingleFirmware()) {
getFwUpgradeNotice();
builder.append(getFwUpgradeNotice());
} else {
builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2()));
}
if (helper.getFirmwareType() != WATCHFACE) {
if (helper.isSingleFirmware()) {
getFwUpgradeNotice();
builder.append(getFwUpgradeNotice());
} else {
builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2()));
}
if (helper.isFirmwareWhitelisted()) {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_known));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version));
// TODO: set a CHECK (OKAY) button
} else {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n")
.append(mContext.getString(R.string.miband_firmware_suggest_whitelist, String.valueOf(helper.getFirmwareVersion())));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version));
// TODO: set a UNKNOWN (question mark) button
if (helper.isFirmwareWhitelisted()) {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_known));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version));
// TODO: set a CHECK (OKAY) button
} else {
builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n")
.append(mContext.getString(R.string.miband_firmware_suggest_whitelist, String.valueOf(helper.getFirmwareVersion())));
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version));
// TODO: set a UNKNOWN (question mark) button
}
}
installActivity.setInfoText(builder.toString());
installActivity.setInstallItem(fwItem);

View File

@ -29,6 +29,7 @@ import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractMiFirmwareInfo;
/**
@ -122,6 +123,11 @@ public class MiBandFWHelper extends AbstractMiBandFWHelper {
firmwareInfo.checkValid();
}
@Override
public HuamiFirmwareType getFirmwareType() {
return null;
}
/**
* @param wholeFirmwareBytes
* @return

View File

@ -27,12 +27,13 @@ import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import de.greenrobot.dao.query.Query;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -219,7 +220,7 @@ public class PebblePairingActivity extends AbstractGBActivity {
private void performConnect(GBDevice gbDevice) {
if (gbDevice == null) {
gbDevice = new GBDevice(mBtDevice.getAddress(), mBtDevice.getName(), DeviceType.PEBBLE);
gbDevice = new GBDevice(mBtDevice.getAddress(), mBtDevice.getName(), null, DeviceType.PEBBLE);
}
GBApplication.deviceService().connect(gbDevice);
}
@ -254,7 +255,7 @@ public class PebblePairingActivity extends AbstractGBActivity {
gbDevice = deviceHelper.toGBDevice(devices.get(0));
gbDevice.setVolatileAddress(btDevice.getAddress());
} catch (Exception e) {
GB.toast("Error retrieving devices from database", Toast.LENGTH_SHORT, GB.ERROR);
GB.toast(getString(R.string.error_retrieving_devices_database), Toast.LENGTH_SHORT, GB.ERROR, e);
returnToPairingActivity();
return null;
}

View File

@ -0,0 +1,135 @@
/* Copyright (C) 2020 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.pinetime;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class PineTimeJFCoordinator extends AbstractDeviceCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && name.startsWith("Pinetime-JF")) {
return DeviceType.PINETIME_JF;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.PINETIME_JF;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public int getAlarmSlotCount() {
return 0;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public String getManufacturer() {
return "Pine64";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
// nothing to delete, yet
}
}

View File

@ -0,0 +1,111 @@
/* Copyright (C) 2020 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.Context;
import android.net.Uri;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class FossilHRInstallHandler implements InstallHandler {
private final Context mContext;
private boolean mIsValid;
private String mVersion = "(Unknown version)";
FossilHRInstallHandler(Uri uri, Context context) {
mContext = context;
UriHelper uriHelper;
try {
uriHelper = UriHelper.get(uri, mContext);
} catch (IOException e) {
mIsValid = false;
return;
}
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
byte[] bytes = new byte[32];
int read = in.read(bytes);
if (read < 32) {
mIsValid = false;
return;
}
ByteBuffer buf = ByteBuffer.wrap(bytes);
buf.order(ByteOrder.LITTLE_ENDIAN);
int header0 = buf.getInt();
buf.getInt(); // size
int header2 = buf.getInt();
int header3 = buf.getInt();
if (header0 != 1 || header2 != 0x00012000 || header3 != 0x00012000) {
mIsValid = false;
return;
}
buf.getInt(); // unknown
int version1 = buf.get() % 0xff;
int version2 = buf.get() & 0xff;
mVersion = "DN1.0." + version1 + "." + version2;
} catch (Exception e) {
mIsValid = false;
return;
}
mIsValid = true;
}
@Override
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
return;
}
if (device.getType() != DeviceType.FOSSILQHYBRID || !device.isConnected()) {
installActivity.setInfoText("Element cannot be installed");
installActivity.setInstallEnabled(false);
return;
}
GenericItem installItem = new GenericItem();
installItem.setIcon(R.drawable.ic_firmware);
installItem.setName("Fossil Hybrid HR Firmware");
installItem.setDetails(mVersion);
installActivity.setInfoText(mContext.getString(R.string.firmware_install_warning, "(unknown)"));
installActivity.setInstallEnabled(true);
installActivity.setInstallItem(installItem);
}
@Override
public void onStartInstall(GBDevice device) {
}
@Override
public boolean isValid() {
return mIsValid;
}
}

View File

@ -1,10 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
@ -26,6 +30,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@ -53,6 +58,9 @@ public class HRConfigActivity extends AbstractGBActivity implements View.OnClick
SparseArray<String> widgetButtonsMapping = new SparseArray<>(4);
static public final String CONFIG_KEY_Q_ACTIONS = "Q_ACTIONS";
private static final int REQUEST_CODE_WIDGET_EDIT = 0;
private static final int REQUEST_CODE_IMAGE_PICK = 1;
private static final int REQUEST_CODE_IMAGE_EDIT = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -83,8 +91,9 @@ public class HRConfigActivity extends AbstractGBActivity implements View.OnClick
Intent startIntent = new Intent(HRConfigActivity.this, WidgetSettingsActivity.class);
startIntent.putExtra("EXTRA_WIDGET", widget);
startIntent.putExtra("EXTRA_WIDGET_IDNEX", position);
startIntent.putExtra("EXTRA_WIDGET_INITIAL_NAME", ((CustomWidget) widget).getName());
startActivityForResult(startIntent, 0);
startActivityForResult(startIntent, REQUEST_CODE_WIDGET_EDIT);
}
});
loadCustomWidgetList();
@ -94,7 +103,36 @@ public class HRConfigActivity extends AbstractGBActivity implements View.OnClick
public void onClick(View v) {
Intent startIntent = new Intent(HRConfigActivity.this, WidgetSettingsActivity.class);
startActivityForResult(startIntent, 0);
startActivityForResult(startIntent, REQUEST_CODE_WIDGET_EDIT);
}
});
findViewById(R.id.qhybrid_set_background).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new AlertDialog.Builder(HRConfigActivity.this)
.setTitle("whoop whoop")
.setMessage("background has to be pushed every time a custom widget changes, causing traffic and battery drain. Consider that when using custom widgets.")
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Intent pickIntent = new Intent(Intent.ACTION_PICK);
pickIntent.setType("image/*");
startActivityForResult(pickIntent, REQUEST_CODE_IMAGE_PICK);
}
})
.setNegativeButton("nah", null)
.show();
}
});
findViewById(R.id.qhybrid_unset_background).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_SET_BACKGROUND_IMAGE);
intent.putIntegerArrayListExtra("EXTRA_PIXELS", null);
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(intent);
}
});
@ -138,34 +176,58 @@ public class HRConfigActivity extends AbstractGBActivity implements View.OnClick
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(data == null) return;
if (resultCode == WidgetSettingsActivity.RESULT_CODE_WIDGET_CREATED) {
CustomWidget widget = (CustomWidget) data.getExtras().get("EXTRA_WIDGET");
this.customWidgets.add(widget);
refreshWidgetList();
saveCustomWidgetList();
if(requestCode == REQUEST_CODE_WIDGET_EDIT) {
if (resultCode == WidgetSettingsActivity.RESULT_CODE_WIDGET_CREATED) {
CustomWidget widget = (CustomWidget) data.getExtras().get("EXTRA_WIDGET");
this.customWidgets.add(widget);
refreshWidgetList();
saveCustomWidgetList();
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHYBRID_COMMAND_UPDATE_WIDGETS));
} else if (resultCode == WidgetSettingsActivity.RESULT_CODE_WIDGET_UPDATED) {
CustomWidget widget = (CustomWidget) data.getExtras().get("EXTRA_WIDGET");
int updateIndex = data.getIntExtra("EXTRA_WIDGET_IDNEX", -1);
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHYBRID_COMMAND_UPDATE_WIDGETS));
} else if (resultCode == WidgetSettingsActivity.RESULT_CODE_WIDGET_UPDATED) {
CustomWidget widget = (CustomWidget) data.getExtras().get("EXTRA_WIDGET");
int updateIndex = data.getIntExtra("EXTRA_WIDGET_IDNEX", -1);
this.customWidgets.set(updateIndex, widget);
String initialName = data.getStringExtra("EXTRA_WIDGET_INITIAL_NAME");
String newName = widget.getName();
refreshWidgetList();
saveCustomWidgetList();
String widgetJSON = sharedPreferences.getString("FOSSIL_HR_WIDGETS", "{}");
widgetJSON = widgetJSON.replace("custom_" + initialName, "custom_" + newName);
sharedPreferences.edit().putString("FOSSIL_HR_WIDGETS", widgetJSON).apply();
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHYBRID_COMMAND_UPDATE_WIDGETS));
} else if (resultCode == WidgetSettingsActivity.RESULT_CODE_WIDGET_DELETED){
int updateIndex = data.getIntExtra("EXTRA_WIDGET_IDNEX", -1);
this.customWidgets.set(updateIndex, widget);
this.customWidgets.remove(updateIndex);
loadWidgetConfigs();
refreshWidgetList();
saveCustomWidgetList();
refreshWidgetList();
saveCustomWidgetList();
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHYBRID_COMMAND_UPDATE_WIDGETS));
} else if (resultCode == WidgetSettingsActivity.RESULT_CODE_WIDGET_DELETED) {
int updateIndex = data.getIntExtra("EXTRA_WIDGET_IDNEX", -1);
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHYBRID_COMMAND_UPDATE_WIDGETS));
this.customWidgets.remove(updateIndex);
refreshWidgetList();
saveCustomWidgetList();
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHYBRID_COMMAND_UPDATE_WIDGETS));
}
}else if(requestCode == REQUEST_CODE_IMAGE_PICK){
if (resultCode == RESULT_OK)
{
Uri imageUri = data.getData();
Intent activityIntent = new Intent();
activityIntent.setClass(this, ImageEditActivity.class);
activityIntent.setData(imageUri);
startActivityForResult(activityIntent, REQUEST_CODE_IMAGE_EDIT);
}
}else if(requestCode == REQUEST_CODE_IMAGE_EDIT){
if(resultCode == ImageEditActivity.RESULT_CODE_EDIT_SUCCESS){
data.setAction(QHybridSupport.QHYBRID_COMMAND_SET_BACKGROUND_IMAGE);
LocalBroadcastManager.getInstance(this).sendBroadcast(data);
}
}
}
private void saveCustomWidgetList() {
@ -474,6 +536,7 @@ public class HRConfigActivity extends AbstractGBActivity implements View.OnClick
super(HRConfigActivity.this, 0, objects);
}
@SuppressLint("ResourceType")
@NonNull
@Override
public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) {

View File

@ -0,0 +1,75 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser.ActivityEntry;
public class HybridHRActivitySampleProvider extends AbstractSampleProvider<HybridHRActivitySample> {
public HybridHRActivitySampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<HybridHRActivitySample, ?> getSampleDao() {
return getSession().getHybridHRActivitySampleDao();
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return null;
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return HybridHRActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return HybridHRActivitySampleDao.Properties.DeviceId;
}
@Override
public int normalizeType(int rawType) {
if(rawType == -1) return 0;
return ActivityEntry.WEARING_STATE.fromValue((byte) rawType).getActivityKind();
}
@Override
public int toRawActivityKind(int activityKind) {
return 0;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / 63f;
}
@Override
public HybridHRActivitySample createActivitySample() {
return new HybridHRActivitySample();
}
@Override
public List<HybridHRActivitySample> getActivitySamples(int timestamp_from, int timestamp_to) {
return super.getActivitySamples(timestamp_from, timestamp_to);
}
@Override
public List<HybridHRActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to) {
return super.getAllActivitySamples(timestamp_from, timestamp_to);
}
}

View File

@ -0,0 +1,260 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.AssetImage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.AssetImageFactory;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ImageEditActivity extends AbstractGBActivity implements View.OnTouchListener {
static final public int RESULT_CODE_EDIT_SUCCESS = 0;
ImageView overlay;
Canvas overlayCanvas;
Paint overlayPaint;
Bitmap overlayBitmap, mainBitmap;
float x = 0, y = 0, diameter = 0, imageDimension = 0;
int imageWidth, imageHeight;
private enum MovementState {
MOVE_UPPER_LEFT,
MOVE_LOWER_RIGHT,
MOVE_FRAME
}
private MovementState movementState;
float movementStartX, movementStartY, movementStartFrameX, movementStartFrameY, movementStartDiameter, leftUpperDeltaX, leftUpperDeltaY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qhybrid_image_edit);
final RelativeLayout mainLayout = findViewById(R.id.qhybrid_image_edit_container);
overlay = findViewById(R.id.qhybrid_image_edit_image_overlay);
overlay.setOnTouchListener(this);
findViewById(R.id.qhybrid_image_edit_okay).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finalizeImage();
}
});
mainLayout.post(new Runnable() {
@Override
public void run() {
try {
fitImageToLayout(mainLayout);
} catch (IOException | RuntimeException e) {
GB.log("Error formatting image", GB.ERROR, e);
GB.toast("Error formatting image", Toast.LENGTH_LONG, GB.ERROR);
finish();
}
}
});
}
private void finalizeImage(){
Bitmap cropped = Bitmap.createBitmap(this.mainBitmap, (int) this.x, (int) this.y, (int) this.diameter, (int) this.diameter);
Bitmap scaled = Bitmap.createScaledBitmap(cropped, 400, 400, false);
cropped.recycle();
try {
AssetImage image = AssetImageFactory.createAssetImage(scaled, false, 0, 0, 0);
Intent resultIntent = new Intent();
resultIntent.putExtra("EXTRA_PIXELS_ENCODED", image.getFileData());
setResult(RESULT_CODE_EDIT_SUCCESS, resultIntent);
finish();
} catch (IOException e) {
e.printStackTrace();
} finally {
scaled.recycle();
}
}
private void fitImageToLayout(RelativeLayout mainLayout) throws IOException, RuntimeException {
float containerHeight = mainLayout.getHeight();
float containerWidth = mainLayout.getWidth();
float containerRelation = containerHeight / containerWidth;
Bitmap bitmap = this.createImageFromURI();
float imageHeight = bitmap.getHeight();
float imageWidth = bitmap.getWidth();
float imageRelation = imageHeight / imageWidth;
float scaleRatio;
if(imageRelation > containerRelation){
scaleRatio = containerHeight / imageHeight;
}else{
scaleRatio = containerWidth / imageWidth;
}
int scaledHeight = (int)(imageHeight * scaleRatio);
int scaledWidth = (int)(imageWidth * scaleRatio);
this.imageHeight = scaledHeight;
this.imageWidth = scaledWidth;
this.imageDimension = this.diameter = Math.min(scaledHeight, scaledWidth);
mainBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false);
ImageView mainImageView = findViewById(R.id.qhybrid_image_edit_image);
mainImageView.setImageBitmap(mainBitmap);
createOverlay(scaledWidth, scaledHeight);
}
private void createOverlay(int width, int height){
this.overlayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
this.overlayCanvas = new Canvas(this.overlayBitmap);
this.overlayPaint = new Paint();
this.overlayPaint.setColor(Color.BLACK);
this.overlayPaint.setStyle(Paint.Style.STROKE);
this.overlayPaint.setStrokeWidth(imageDimension / 100);
renderOverlay();
}
private Bitmap createImageFromURI() throws IOException, RuntimeException {
Uri imageURI = getIntent().getData();
if (imageURI == null) {
throw new RuntimeException("no image attached");
}
ContentResolver resolver = getContentResolver();
Cursor c = resolver.query(imageURI, new String[]{MediaStore.Images.ImageColumns.ORIENTATION}, null, null, null);
c.moveToFirst();
int orientation = c.getInt(c.getColumnIndex(MediaStore.Images.ImageColumns.ORIENTATION));
c.close();
Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), imageURI);
if (orientation != 0) {
Matrix matrix = new Matrix();
matrix.postRotate(90);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
return bitmap;
}
private void renderOverlay() {
overlayCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
overlayCanvas.drawCircle(x, y, imageDimension / 15, overlayPaint);
overlayCanvas.drawCircle(x + diameter, y + diameter, imageDimension / 15, overlayPaint);
overlayCanvas.drawCircle(x + diameter / 2, y + diameter / 2, diameter / 2, overlayPaint);
overlay.setImageBitmap(overlayBitmap);
}
private void forceImageBoundaries(){
this.x = Math.max(this.x, 0);
this.y = Math.max(this.y, 0);
this.x = Math.min(this.x, this.imageWidth - diameter);
this.y = Math.min(this.y, this.imageHeight - diameter);
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if(motionEvent.getAction() == MotionEvent.ACTION_DOWN){
handleTouchDown(motionEvent);
}else if(motionEvent.getAction() == MotionEvent.ACTION_MOVE){
handleTouchMove(motionEvent);
}
return true;
}
private void handleTouchMove(MotionEvent motionEvent){
if(movementState == MovementState.MOVE_UPPER_LEFT){
float moveDeltaX = motionEvent.getX() - this.movementStartX;
float moveDeltaY = motionEvent.getY() - this.movementStartY;
float mid = (moveDeltaX + moveDeltaY) / 2;
this.diameter = this.movementStartDiameter - mid;
this.x = this.movementStartX + mid + this.leftUpperDeltaX;
this.y = this.movementStartY + mid + this.leftUpperDeltaY;
}else if(movementState == MovementState.MOVE_LOWER_RIGHT) {
float moveDeltaX = motionEvent.getX() - this.movementStartX;
float moveDeltaY = motionEvent.getY() - this.movementStartY;
float mid = (moveDeltaX + moveDeltaY) / 2;
this.diameter = this.movementStartDiameter + mid;
}else if(movementState == MovementState.MOVE_FRAME){
this.x = this.movementStartFrameX + (motionEvent.getX() - this.movementStartX);
this.y = this.movementStartFrameY + (motionEvent.getY() - this.movementStartY);
}
this.forceImageBoundaries();
renderOverlay();
}
private void handleTouchDown(MotionEvent motionEvent) {
this.movementStartX = motionEvent.getX();
this.movementStartY = motionEvent.getY();
this.movementStartFrameX = this.x;
this.movementStartFrameY = this.y;
this.movementStartDiameter = this.diameter;
final float threshold = imageDimension / 15;
float upperLeftDeltaX = this.x - motionEvent.getX();
float upperLeftDeltaY = this.y - motionEvent.getY();
float upperLeftDistance = (float) Math.sqrt(upperLeftDeltaX * upperLeftDeltaX + upperLeftDeltaY * upperLeftDeltaY);
if(upperLeftDistance < threshold){
// Toast.makeText(this, "upper left", 0).show();
this.leftUpperDeltaX = upperLeftDeltaX;
this.leftUpperDeltaY = upperLeftDeltaY;
this.movementState = MovementState.MOVE_UPPER_LEFT;
return;
}
float lowerLeftX = this.x + diameter;
float lowerLeftY = this.y + diameter;
float lowerRightDeltaX = lowerLeftX - motionEvent.getX();
float lowerRightDeltaY = lowerLeftY - motionEvent.getY();
float lowerRightDistance = (float) Math.sqrt(lowerRightDeltaX * lowerRightDeltaX + lowerRightDeltaY * lowerRightDeltaY);
if(lowerRightDistance < threshold){
// Toast.makeText(this, "lower right", 0).show();
this.movementState = MovementState.MOVE_LOWER_RIGHT;
return;
}
// Toast.makeText(this, "anywhere else", 0).show();
this.movementState = MovementState.MOVE_FRAME;
}
}

View File

@ -85,7 +85,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
@Override
public boolean supportsActivityTracking() {
return false;
return true;
}
@Override
@ -95,11 +95,15 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
return new HybridHRActivitySampleProvider(device, session);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
if (isHybridHR()) {
FossilHRInstallHandler installHandler = new FossilHRInstallHandler(uri, context);
return installHandler.isValid() ? installHandler : null;
}
return null;
}
@ -121,6 +125,11 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
return this.supportsAlarmConfiguration() ? 5 : 0;
}
@Override
public boolean supportsAlarmDescription(GBDevice device) {
return isHybridHR();
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
@ -128,7 +137,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
return this.isHybridHR();
}
@Override

View File

@ -7,6 +7,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.RadioButton;

View File

@ -0,0 +1,62 @@
/* Copyright (C) 2020 Erik Bloß
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.tlw64;
import java.util.UUID;
public final class TLW64Constants {
public static final UUID UUID_SERVICE_NO1 = UUID.fromString("000055ff-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("000033f1-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_NOTIFY = UUID.fromString("000033f2-0000-1000-8000-00805f9b34fb");
// Command bytes
public static final byte CMD_DISPLAY_SETTINGS = (byte) 0xa0;
public static final byte CMD_FIRMWARE_VERSION = (byte) 0xa1;
public static final byte CMD_BATTERY = (byte) 0xa2;
public static final byte CMD_DATETIME = (byte) 0xa3;
public static final byte CMD_USER_DATA = (byte) 0xa9;
public static final byte CMD_ALARM = (byte) 0xab;
public static final byte CMD_FACTORY_RESET = (byte) 0xad;
public static final byte CMD_FETCH_STEPS = (byte) 0xb2;
public static final byte CMD_FETCH_SLEEP = (byte) 0xb3;
public static final byte CMD_NOTIFICATION = (byte) 0xc1;
public static final byte CMD_ICON = (byte) 0xc3;
public static final byte CMD_DEVICE_SETTINGS = (byte) 0xd3;
// Notifications
public static final byte NOTIFICATION_HEADER = (byte) 0x01;
public static final byte NOTIFICATION_CALL = (byte) 0x02; // displays "call" on screen
public static final byte NOTIFICATION_SMS = (byte) 0x03; // displays "mms" on screen
public static final byte NOTIFICATION_STOP = (byte) 0x04; // to stop showing incoming call
// Icons
public static final byte ICON_QQ = (byte) 0x01;
public static final byte ICON_WECHAT = (byte) 0x02;
public static final byte ICON_MAIL = (byte) 0x04;
// Alarm arguments
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SUNDAY = (byte) 0x01;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_MONDAY = (byte) 0x02;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_TUESDAY = (byte) 0x04;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_WEDNESDAY = (byte) 0x08;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_THURSDAY = (byte) 0x10;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_FRIDAY = (byte) 0x20;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SATURDAY = (byte) 0x40;
}

View File

@ -0,0 +1,151 @@
/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, José Rebelo, Matthieu Baerts, Nephiel, vanous, Erik Bloß
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.tlw64;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class TLW64Coordinator extends AbstractDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && name.startsWith("Smart Bee")) {
return DeviceType.TLW64;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.TLW64;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
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 TLW64SampleProvider(device, session);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public int getAlarmSlotCount() {
return 3;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public String getManufacturer() {
return "Toleda";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_liftwrist_display_noshed,
R.xml.devicesettings_longsit_noshed,
R.xml.devicesettings_timeformat
};
}
}

View File

@ -0,0 +1,68 @@
package nodomain.freeyourgadget.gadgetbridge.devices.tlw64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.TLW64ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.TLW64ActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class TLW64SampleProvider extends AbstractSampleProvider<TLW64ActivitySample> {
private GBDevice mDevice;
private DaoSession mSession;
public TLW64SampleProvider(GBDevice device, DaoSession session) {
super(device, session);
mSession = session;
mDevice = device;
}
@Override
public AbstractDao<TLW64ActivitySample, ?> getSampleDao() {
return getSession().getTLW64ActivitySampleDao();
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return TLW64ActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return TLW64ActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return TLW64ActivitySampleDao.Properties.DeviceId;
}
@Override
public int normalizeType(int rawType) {
return rawType;
}
@Override
public int toRawActivityKind(int activityKind) {
return activityKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / (float) 4000.0;
}
@Override
public TLW64ActivitySample createActivitySample() {
return new TLW64ActivitySample();
}
}

View File

@ -63,7 +63,9 @@ public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator {
String deviceName = candidate.getName().toUpperCase();
if (candidate.supportsService(Watch9Constants.UUID_SERVICE_WATCH9)) {
return DeviceType.WATCH9;
} else if (macAddress.startsWith("1C:87:79")) {
// add support for Watch X non-plus (same MAC address)
// add support for Watch X Plus (same MAC address)
} else if ((macAddress.startsWith("1C:87:79")) && ((!deviceName.equalsIgnoreCase("WATCH X")) && (!deviceName.equalsIgnoreCase("WATCH XPLUS")))) {
return DeviceType.WATCH9;
} else if (deviceName.equals("WATCH 9")) {
return DeviceType.WATCH9;
@ -161,4 +163,10 @@ public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator {
public boolean supportsFindDevice() {
return false;
}
@Nullable
@Override
public Class<? extends Activity> getCalibrationActivity() {
return Watch9CalibrationActivity.class;
}
}

View File

@ -80,6 +80,7 @@ public class ZeTimeConstants {
public static final byte CMD_REMINDERS = (byte) 0x97;
public static final byte CMD_PUSH_CALENDAR_DAY = (byte) 0x99;
public static final byte CMD_MUSIC_CONTROL = (byte) 0xD0;
public static final byte CMD_CALL_CONTROL = (byte) 0xDC;
public static final byte CMD_TEST_SIGNALING = (byte) 0xFA;
// here are the action commands
public static final byte CMD_REQUEST = (byte) 0x70;

View File

@ -124,7 +124,7 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
@Override
public boolean supportsCalendarEvents() {
return true;
return false;
}
@Override
@ -163,8 +163,10 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_zetime,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_sync_calendar,
};
}
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.entities;
public abstract class AbstractHybridHRActivitySample extends AbstractActivitySample {
abstract public int getCalories();
abstract public byte getWear_type();
@Override
public int getRawKind() {
return getWear_type();
}
@Override
public int getRawIntensity() {
return getCalories();
}
@Override
public void setUserId(long userId) {}
@Override
public long getUserId() {
return 0;
}
}

View File

@ -48,7 +48,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
return;
}
LOG.info("Bluetooth turned on => connecting...");
LOG.info("Bluetooth turned on (ACTION_STATE_CHANGED) => connecting...");
GBApplication.deviceService().connect();
} else if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_OFF) {
LOG.info("Bluetooth turned off => disconnecting...");

View File

@ -101,7 +101,7 @@ public class CalendarReceiver extends BroadcastReceiver {
DaoSession session = dbHandler.getDaoSession();
syncCalendar(eventList, session);
} catch (Exception e1) {
GB.toast("Database Error while syncing Calendar", Toast.LENGTH_SHORT, GB.ERROR);
GB.toast("Database Error while syncing Calendar", Toast.LENGTH_SHORT, GB.ERROR, e1);
}
}

View File

@ -33,6 +33,7 @@ import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService;
@ -118,6 +119,10 @@ public class NotificationListener extends NotificationListenerService {
private long activeCallPostTime;
private int mLastCallCommand = CallSpec.CALL_UNDEFINED;
private Handler mHandler = new Handler();
private Runnable mSetMusicInfoRunnable = null;
private Runnable mSetMusicStateRunnable = null;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
@ -276,15 +281,18 @@ public class NotificationListener extends NotificationListenerService {
Prefs prefs = GBApplication.getPrefs();
if (GBApplication.isRunningLollipopOrLater()) {
if (NotificationCompat.CATEGORY_CALL.equals(sbn.getNotification().category)
&& prefs.getBoolean("notification_support_voip_calls", false)) {
&& prefs.getBoolean("notification_support_voip_calls", false)
&& sbn.isOngoing()) {
handleCallNotification(sbn);
return;
}
}
if (shouldIgnoreNotification(sbn)) {
LOG.info("Ignoring notification");
return;
if (!"com.sec.android.app.clockpackage".equals(sbn.getPackageName())) { // workaround to allow phone alarm notification
LOG.info("Ignore notification: " + sbn.getPackageName()); // need to fix
return;
}
}
String source = sbn.getPackageName().toLowerCase();
@ -460,7 +468,7 @@ public class NotificationListener extends NotificationListenerService {
private void handleCallNotification(StatusBarNotification sbn) {
String app = sbn.getPackageName();
LOG.debug("got call from: " + app);
if(app.equals("com.android.dialer")) {
if (app.equals("com.android.dialer") || app.equals("com.android.incallui")) {
LOG.debug("Ignoring non-voip call");
return;
}
@ -616,8 +624,8 @@ public class NotificationListener extends NotificationListenerService {
* @return true if notification was handled, false otherwise
*/
public boolean handleMediaSessionNotification(MediaSessionCompat.Token mediaSession) {
MusicSpec musicSpec = new MusicSpec();
MusicStateSpec stateSpec = new MusicStateSpec();
final MusicSpec musicSpec = new MusicSpec();
final MusicStateSpec stateSpec = new MusicStateSpec();
MediaControllerCompat c;
try {
@ -660,8 +668,27 @@ public class NotificationListener extends NotificationListenerService {
musicSpec.trackNr = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER);
// finally, tell the device about it
GBApplication.deviceService().onSetMusicInfo(musicSpec);
GBApplication.deviceService().onSetMusicState(stateSpec);
if (mSetMusicInfoRunnable != null) {
mHandler.removeCallbacks(mSetMusicInfoRunnable);
}
mSetMusicInfoRunnable = new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSetMusicInfo(musicSpec);
}
};
mHandler.postDelayed(mSetMusicInfoRunnable, 100);
if (mSetMusicStateRunnable != null) {
mHandler.removeCallbacks(mSetMusicStateRunnable);
}
mSetMusicStateRunnable = new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSetMusicState(stateSpec);
}
};
mHandler.postDelayed(mSetMusicStateRunnable, 100);
return true;
} catch (NullPointerException | RemoteException e) {
@ -794,7 +821,7 @@ public class NotificationListener extends NotificationListenerService {
if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) {
PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE);
if (powermanager != null && 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 true;
}
}

View File

@ -26,12 +26,16 @@ import android.content.Intent;
import android.media.AudioManager;
import android.telephony.TelephonyManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class PhoneCallReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(PhoneCallReceiver.class);
private static int mLastState = TelephonyManager.CALL_STATE_IDLE;
private static String mSavedNumber;
@ -45,12 +49,16 @@ public class PhoneCallReceiver extends BroadcastReceiver {
mSavedNumber = intent.getExtras().getString("android.intent.extra.PHONE_NUMBER");
} else if(intent.getAction().equals("nodomain.freeyourgadget.gadgetbridge.MUTE_CALL")) {
// Handle the mute request only if the phone is currently ringing
if(mLastState != TelephonyManager.CALL_STATE_RINGING)
if (mLastState != TelephonyManager.CALL_STATE_RINGING)
return;
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mLastRingerMode = audioManager.getRingerMode();
audioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
try {
audioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
} catch (SecurityException e) {
LOG.error("SecurityException when trying to set ringer (no permission granted :/ ?), not setting it then.");
}
mRestoreMutedCall = true;
} else {
if (intent.hasExtra(TelephonyManager.EXTRA_INCOMING_NUMBER)) {

View File

@ -0,0 +1,44 @@
/* Copyright (C) 2020 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
public class TinyWeatherForecastGermanyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
WeatherSpec weatherSpec = bundle.getParcelable("WeatherSpec");
if (weatherSpec != null) {
Weather.getInstance().setWeatherSpec(weatherSpec);
GBApplication.deviceService().onSendWeather(weatherSpec);
}
}
}
}
}

View File

@ -22,6 +22,10 @@ import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -30,9 +34,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
@ -66,6 +67,7 @@ public class GBDevice implements Parcelable {
private static final String DEVINFO_ADDR = "ADDR: ";
private static final String DEVINFO_ADDR2 = "ADDR2: ";
private String mName;
private String mAlias;
private final String mAddress;
private String mVolatileAddress;
private final DeviceType mDeviceType;
@ -86,20 +88,22 @@ public class GBDevice implements Parcelable {
private int mNotificationIconDisconnected = R.drawable.ic_notification_disconnected;
private int mNotificationIconLowBattery = R.drawable.ic_notification_low_battery;
public GBDevice(String address, String name, DeviceType deviceType) {
this(address, null, name, deviceType);
public GBDevice(String address, String name, String alias, DeviceType deviceType) {
this(address, null, name, alias, deviceType);
}
public GBDevice(String address, String address2, String name, DeviceType deviceType) {
public GBDevice(String address, String address2, String name, String alias, DeviceType deviceType) {
mAddress = address;
mVolatileAddress = address2;
mName = (name != null) ? name : mAddress;
mAlias = alias;
mDeviceType = deviceType;
validate();
}
private GBDevice(Parcel in) {
mName = in.readString();
mAlias = in.readString();
mAddress = in.readString();
mVolatileAddress = in.readString();
mDeviceType = DeviceType.values()[in.readInt()];
@ -124,6 +128,7 @@ public class GBDevice implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeString(mAlias);
dest.writeString(mAddress);
dest.writeString(mVolatileAddress);
dest.writeInt(mDeviceType.ordinal());
@ -149,10 +154,22 @@ public class GBDevice implements Parcelable {
}
}
public String getName() {
return mName;
}
public String getAlias() {
return mAlias;
}
public String getAliasOrName() {
if (mAlias != null && !mAlias.equals("")) {
return mAlias;
}
return mName;
}
public void setName(String name) {
if (name == null) {
LOG.warn("Ignoring setting of GBDevice name to null for " + this);
@ -161,6 +178,10 @@ public class GBDevice implements Parcelable {
mName = name;
}
public void setAlias(String alias) {
mAlias = alias;
}
public String getAddress() {
return mAddress;
}

View File

@ -19,27 +19,33 @@ package nodomain.freeyourgadget.gadgetbridge.model;
import android.content.Context;
import androidx.annotation.DrawableRes;
import java.util.Arrays;
import androidx.annotation.DrawableRes;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
public class ActivityKind {
public static final int TYPE_NOT_MEASURED = -1;
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_ACTIVITY = 1;
public static final int TYPE_LIGHT_SLEEP = 2;
public static final int TYPE_DEEP_SLEEP = 4;
public static final int TYPE_NOT_WORN = 8;
public static final int TYPE_RUNNING = 16;
public static final int TYPE_WALKING = 32;
public static final int TYPE_SWIMMING = 64;
public static final int TYPE_CYCLING = 128;
public static final int TYPE_TREADMILL = 256;
public static final int TYPE_EXERCISE = 512;
public static final int TYPE_UNKNOWN = 0x00000000;
public static final int TYPE_ACTIVITY = 0x00000001;
public static final int TYPE_LIGHT_SLEEP = 0x00000002;
public static final int TYPE_DEEP_SLEEP = 0x00000004;
public static final int TYPE_NOT_WORN = 0x00000008;
public static final int TYPE_RUNNING = 0x00000010;
public static final int TYPE_WALKING = 0x00000020;
public static final int TYPE_SWIMMING = 0x00000040;
public static final int TYPE_CYCLING = 0x00000080;
public static final int TYPE_TREADMILL = 0x00000100;
public static final int TYPE_EXERCISE = 0x00000200;
public static final int TYPE_SWIMMING_OPENWATER = 0x00000400;
public static final int TYPE_INDOOR_CYCLING = 0x00000800;
public static final int TYPE_ELLIPTICAL_TRAINER = 0x00001000;
public static final int TYPE_JUMP_ROPING = 0x00002000;
public static final int TYPE_YOGA = 0x00004000;
private static final int TYPES_COUNT = 12;
private static final int TYPES_COUNT = 17;
public static final int TYPE_SLEEP = TYPE_LIGHT_SLEEP | TYPE_DEEP_SLEEP;
public static final int TYPE_ALL = TYPE_ACTIVITY | TYPE_SLEEP | TYPE_NOT_WORN;
@ -77,6 +83,21 @@ public class ActivityKind {
if ((types & ActivityKind.TYPE_EXERCISE) != 0) {
result[i++] = provider.toRawActivityKind(TYPE_EXERCISE);
}
if ((types & ActivityKind.TYPE_SWIMMING_OPENWATER) != 0) {
result[i++] = provider.toRawActivityKind(TYPE_SWIMMING_OPENWATER);
}
if ((types & ActivityKind.TYPE_INDOOR_CYCLING) != 0) {
result[i++] = provider.toRawActivityKind(TYPE_INDOOR_CYCLING);
}
if ((types & ActivityKind.TYPE_ELLIPTICAL_TRAINER) != 0) {
result[i++] = provider.toRawActivityKind(TYPE_ELLIPTICAL_TRAINER);
}
if ((types & ActivityKind.TYPE_JUMP_ROPING) != 0) {
result[i++] = provider.toRawActivityKind(TYPE_JUMP_ROPING);
}
if ((types & ActivityKind.TYPE_YOGA) != 0) {
result[i++] = provider.toRawActivityKind(TYPE_YOGA);
}
return Arrays.copyOf(result, i);
}
@ -104,6 +125,16 @@ public class ActivityKind {
return context.getString(R.string.activity_type_treadmill);
case TYPE_EXERCISE:
return context.getString(R.string.activity_type_exercise);
case TYPE_SWIMMING_OPENWATER:
return context.getString(R.string.activity_type_swimming_openwater);
case TYPE_INDOOR_CYCLING:
return context.getString(R.string.activity_type_indoor_cycling);
case TYPE_ELLIPTICAL_TRAINER:
return context.getString(R.string.activity_type_elliptical_trainer);
case TYPE_JUMP_ROPING:
return context.getString(R.string.activity_type_jump_roping);
case TYPE_YOGA:
return context.getString(R.string.activity_type_yoga);
case TYPE_UNKNOWN:
default:
return context.getString(R.string.activity_type_unknown);
@ -127,10 +158,19 @@ public class ActivityKind {
return R.drawable.ic_activity_biking;
case TYPE_TREADMILL:
return R.drawable.ic_activity_walking;
case TYPE_EXERCISE: // fall through
case TYPE_EXERCISE:
return R.drawable.ic_activity_exercise;
case TYPE_SWIMMING: // fall through
case TYPE_SWIMMING:
case TYPE_SWIMMING_OPENWATER:
return R.drawable.ic_activity_swimming;
case TYPE_INDOOR_CYCLING:
return R.drawable.ic_activity_biking; // TODO: Find a better one
case TYPE_ELLIPTICAL_TRAINER:
return R.drawable.ic_activity_walking; // TODO: Find a better one
case TYPE_JUMP_ROPING:
return R.drawable.ic_activity_exercise; // TODO: Find a better one
case TYPE_YOGA:
return R.drawable.ic_activity_exercise; // TODO: Find a better one
case TYPE_NOT_WORN: // fall through
case TYPE_ACTIVITY: // fall through
case TYPE_UNKNOWN: // fall through

View File

@ -16,6 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.Date;
@ -25,6 +27,7 @@ import java.util.Date;
* // TODO: split into separate entities?
*/
public interface ActivitySummary extends Serializable {
Long getId();
String getName();
Date getStartTime();
Date getEndTime();
@ -35,6 +38,7 @@ public interface ActivitySummary extends Serializable {
long getDeviceId();
long getUserId();
String getSummaryData();
// long getSteps();
// float getDistanceMeters();
// float getAscentMeters();

View File

@ -0,0 +1,57 @@
package nodomain.freeyourgadget.gadgetbridge.model;
import android.content.Context;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class ActivitySummaryItems {
private final GBDevice device;
private int activityKindFilter;
List<BaseActivitySummary> allItems;
ActivitySummariesAdapter itemsAdapter;
private int current_position = 0;
public ActivitySummaryItems(Context context, GBDevice device, int activityKindFilter) {
this.device = device;
this.activityKindFilter = activityKindFilter;
this.itemsAdapter = new ActivitySummariesAdapter(context, device, activityKindFilter);
}
public BaseActivitySummary getItem(int position){
current_position=position;
return itemsAdapter.getItem(position);
}
public int getPosition(BaseActivitySummary item){
return itemsAdapter.getPosition(item);
}
public List<BaseActivitySummary> getAllItems(){
return itemsAdapter.getItems();
}
public BaseActivitySummary getNextItem(){
if (current_position+1 < itemsAdapter.getCount()){
current_position+=1;
return itemsAdapter.getItem(current_position);
}
return null;
}
public BaseActivitySummary getPrevItem(){
if (current_position-1 >= 0){
current_position-=1;
return itemsAdapter.getItem(current_position);
}
return null;
}
public int getCurrent_position(){
return current_position;
}
}

View File

@ -52,4 +52,8 @@ public interface Alarm extends Serializable {
int getHour();
int getMinute();
String getTitle();
String getDescription();
}

View File

@ -104,7 +104,7 @@ public class DailyTotals {
}
private long getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) {
public long getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) {
long totalSteps = 0;
for (ActivityAmount amount : activityAmounts.getAmounts()) {
@ -131,7 +131,7 @@ public class DailyTotals {
}
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
public List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return getAllSamples(db, device, tsFrom, tsTo);
}

View File

@ -35,15 +35,18 @@ public enum DeviceType {
PEBBLE(1, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_pebble),
MIBAND(10, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled, R.string.devicetype_miband),
MIBAND2(11, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband2),
AMAZFITBIP(12, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_bip),
AMAZFITBIP(12, R.drawable.ic_device_amazfit_bip, R.drawable.ic_device_amazfit_bip_disabled, R.string.devicetype_amazfit_bip),
AMAZFITCOR(13, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_amazfit_cor),
MIBAND3(14, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband3),
AMAZFITCOR2(15, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_amazfit_cor2),
MIBAND4(16, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband4),
AMAZFITBIP_LITE(17, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_bip_lite),
AMAZFITGTR(18, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_gtr),
AMAZFITGTS(19, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_gts),
AMAZFITBIPS(20, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_bips),
AMAZFITBIP_LITE(17, R.drawable.ic_device_amazfit_bip, R.drawable.ic_device_amazfit_bip_disabled, R.string.devicetype_amazfit_bip_lite),
AMAZFITGTR(18, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_amazfit_gtr),
AMAZFITGTS(19, R.drawable.ic_device_amazfit_bip, R.drawable.ic_device_amazfit_bip_disabled, R.string.devicetype_amazfit_gts),
AMAZFITBIPS(20, R.drawable.ic_device_amazfit_bip, R.drawable.ic_device_amazfit_bip_disabled, R.string.devicetype_amazfit_bips),
AMAZFITGTR_LITE(21, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_amazfit_gtr),
AMAZFITTREX(22, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_amazfit_trex),
MIBAND5(23, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband5),
LIVEVIEW(30, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_liveview),
HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_hplus),
MAKIBESF68(41, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_makibes_f68),
@ -56,6 +59,8 @@ public enum DeviceType {
ZETIME(80, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_mykronoz_zetime),
ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115),
WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9),
WATCHX(101, R.drawable.ic_device_watchxplus, R.drawable.ic_device_watchxplus_disabled, R.string.devicetype_watchx),
WATCHXPLUS(102, R.drawable.ic_device_watchxplus, R.drawable.ic_device_watchxplus_disabled, R.string.devicetype_watchxplus),
ROIDMI(110, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi),
ROIDMI3(112, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3),
CASIOGB6900(120, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogb6900),
@ -64,11 +69,12 @@ public enum DeviceType {
MAKIBESHR3(150, R.drawable.ic_device_default, R.drawable.ic_device_hplus_disabled, R.string.devicetype_makibes_hr3),
BANGLEJS(160, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_banglejs),
FOSSILQHYBRID(170, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_qhybrid),
TLW64(180, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_tlw64),
PINETIME_JF(190, R.drawable.ic_device_pinetime, R.drawable.ic_device_pinetime_disabled, R.string.devicetype_pinetime_jf),
MIJIA_LYWSD02(200, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_mijia_lywsd02),
ITAG(250, R.drawable.ic_device_itag, R.drawable.ic_device_itag_disabled, R.string.devicetype_itag),
VIBRATISSIMO(300, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_vibratissimo),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
private final int key;
@DrawableRes
private final int defaultIcon;

View File

@ -63,6 +63,7 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.PebbleReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.PhoneCallReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGermanyReceiver;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
@ -197,6 +198,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private CalendarReceiver mCalendarReceiver = null;
private CMWeatherReceiver mCMWeatherReceiver = null;
private LineageOsWeatherReceiver mLineageOsWeatherReceiver = null;
private TinyWeatherForecastGermanyReceiver mTinyWeatherForecastGermanyReceiver = null;
private OmniJawsObserver mOmniJawsObserver = null;
private final String[] mMusicActions = {
@ -735,25 +737,35 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
filter.addAction(AlarmClockReceiver.GOOGLE_CLOCK_ALARM_DONE_ACTION);
registerReceiver(mAlarmClockReceiver, filter);
}
if (mCMWeatherReceiver == null && coordinator != null && coordinator.supportsWeather()) {
mCMWeatherReceiver = new CMWeatherReceiver();
registerReceiver(mCMWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER"));
}
if (GBApplication.isRunningOreoOrLater()) {
if (mLineageOsWeatherReceiver == null && coordinator != null && coordinator.supportsWeather()) {
mLineageOsWeatherReceiver = new LineageOsWeatherReceiver();
registerReceiver(mLineageOsWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER"));
}
}
if (mOmniJawsObserver == null && coordinator != null && coordinator.supportsWeather()) {
try {
mOmniJawsObserver = new OmniJawsObserver(new Handler());
getContentResolver().registerContentObserver(OmniJawsObserver.WEATHER_URI, true, mOmniJawsObserver);
} catch (PackageManager.NameNotFoundException e) {
//Nothing wrong, it just means we're not running on omnirom.
// Weather receivers
if ( coordinator != null && coordinator.supportsWeather()) {
if (GBApplication.isRunningOreoOrLater()) {
if (mLineageOsWeatherReceiver == null) {
mLineageOsWeatherReceiver = new LineageOsWeatherReceiver();
registerReceiver(mLineageOsWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER"));
}
}
else {
if (mCMWeatherReceiver == null) {
mCMWeatherReceiver = new CMWeatherReceiver();
registerReceiver(mCMWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER"));
}
}
if (mTinyWeatherForecastGermanyReceiver == null) {
mTinyWeatherForecastGermanyReceiver = new TinyWeatherForecastGermanyReceiver();
registerReceiver(mTinyWeatherForecastGermanyReceiver, new IntentFilter("de.kaffeemitkoffein.broadcast.WEATHERDATA"));
}
if (mOmniJawsObserver == null) {
try {
mOmniJawsObserver = new OmniJawsObserver(new Handler());
getContentResolver().registerContentObserver(OmniJawsObserver.WEATHER_URI, true, mOmniJawsObserver);
} catch (PackageManager.NameNotFoundException e) {
//Nothing wrong, it just means we're not running on omnirom.
}
}
}
if (GBApplication.getPrefs().getBoolean("auto_fetch_enabled", false) &&
coordinator != null && coordinator.supportsActivityDataFetching() && mGBAutoFetchReceiver == null) {
mGBAutoFetchReceiver = new GBAutoFetchReceiver();
@ -807,6 +819,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
if (mOmniJawsObserver != null) {
getContentResolver().unregisterContentObserver(mOmniJawsObserver);
mOmniJawsObserver = null;
}
if (mTinyWeatherForecastGermanyReceiver != null) {
unregisterReceiver(mTinyWeatherForecastGermanyReceiver);
mTinyWeatherForecastGermanyReceiver = null;
}
if (mGBAutoFetchReceiver != null) {
unregisterReceiver(mGBAutoFetchReceiver);

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