diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index cf4686c60..91137f70d 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -15,8 +15,7 @@ If you just have a question, please ask first in the user chatroom in Matrix: `# ### I got Gadgetbridge from: * [ ] F-Droid * [ ] I built it myself from source code (specify tag / commit) - -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. +* [ ] I previously used Gadgetbridge from other sources and then updated to F-Droid version #### Your issue is: *If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.* @@ -35,7 +34,7 @@ Here go lines of your log. *Please specify model and firmware version if possible* -#### Your android version is: +#### Your Android version/manufacturer flavor is: #### Your Gadgetbridge version is: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cf4686c60..91137f70d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,8 +15,7 @@ If you just have a question, please ask first in the user chatroom in Matrix: `# ### I got Gadgetbridge from: * [ ] F-Droid * [ ] I built it myself from source code (specify tag / commit) - -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. +* [ ] I previously used Gadgetbridge from other sources and then updated to F-Droid version #### Your issue is: *If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.* @@ -35,7 +34,7 @@ Here go lines of your log. *Please specify model and firmware version if possible* -#### Your android version is: +#### Your Android version/manufacturer flavor is: #### Your Gadgetbridge version is: diff --git a/.github/ISSUE_TEMPLATE/device_request.md b/.github/ISSUE_TEMPLATE/device_request.md index a3b1e121a..e6b67390d 100644 --- a/.github/ISSUE_TEMPLATE/device_request.md +++ b/.github/ISSUE_TEMPLATE/device_request.md @@ -14,9 +14,15 @@ You can use the `Preview` tab ^ above to see final rendering of your report. Use #### Device information +- Adding an implementation for a new device requires a "willing to learn" developer, ideally with the device at hand. Without that, you may try to submit a device request and see if anyone steps up and implements it. + + - Provide device name, manufacturer and similarity to other devices: +- Ideally, use an Android Bluetooth scanner app like nRF Connect or BLExplorer and provide screenshots of the scanned device from that app. This provides a name and some available UUIDs, which are needed for implementation. You may want to blur a MAC address for privacy reasons. + + - Specify model and firmware version if possible: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index f2190462b..7283c0b3d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -28,7 +28,7 @@ Here go lines of your log. *Please specify model and firmware version if possible* -#### Your android version is: +#### Your Android version/manufacturer flavor is: #### Your Gadgetbridge version is: diff --git a/.gitmodules b/.gitmodules index 4c058130a..19b292ea4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "fossil-hr-watchface"] path = external/fossil-hr-watchface - url = https://github.com/arjan-s/fossil-hr-watchface + url = https://codeberg.org/Freeyourgadget/fossil-hr-watchface [submodule "jerryscript"] path = external/jerryscript url = https://github.com/jerryscript-project/jerryscript diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d3b852e..9ec0f27c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ ### Changelog +### 0.66.0 +* Add basic support for Casio GBD-H1000 +* Add support for Hama Fit Track 1900 - via FitPro device support +* Add OpenTracksController for interactions with OpenTracks +* Fossil Hybrid HR: Start/stop track in OpenTracks from GPS workout on watch +* Fossil Hybrid HR: Try guessing new widget position +* Fossil Hybrid HR: Allow assigning no function to a button +* Add Huami button/device action to control fitness tracking via OpenTracksController +* Mi Band 6: Sync alarms set on the watch like on Amazfit Bip U and others +* Bangle.js: Handle battery charging status and fix battery chart. +* Bangle.js: Prevent exception in case UART RX line is empty +* Bangle.js: Add repetitions in alarm JSON +* WaspOS: Fix battery chart. +* WaspOS: Add condition code to weather JSON +* XWatch: Add notifications and calls support +* UM-25: Make cumulative values resettable +* VESC: Fixed crash when loading a saved value +* Allow to open Android notification settings from Notification settings +* AutoExporter changes for better operation and troubleshooting +* Change Nightly icons background color + ### 0.65.0 * Amazfit Pop/Pro: Initial Support (probably the same as Bip U but has a different firmware) * Sony WH-1000XM4: Initial Support diff --git a/app/build.gradle b/app/build.gradle index 5a34e5732..1a03e3b5a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,8 +56,8 @@ android { multiDexEnabled true // Note: always bump BOTH versionCode and versionName! - versionName "0.65.0" - versionCode 209 + versionName "0.66.0" + versionCode 210 vectorDrawables.useSupportLibrary = true multiDexEnabled true buildConfigField "String", "GIT_HASH_SHORT", "\"${getGitHashShort()}\"" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0cd67ca85..6af6c8ff1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,6 +18,10 @@ -keepclassmembers class nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface { public *; } +# Required for refection in BangleJSDeviceSupport +-keepclassmembers class nodomain.freeyourgadget.gadgetbridge.model.CallSpec { + public static *; +} -keepattributes JavascriptInterface # https://github.com/tony19/logback-android/issues/29 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fac9decb4..edf7b965e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -668,5 +668,10 @@ + + diff --git a/app/src/main/assets/fossil_hr/battery_layout.json b/app/src/main/assets/fossil_hr/battery_layout.json new file mode 100644 index 000000000..4d35d4c85 --- /dev/null +++ b/app/src/main/assets/fossil_hr/battery_layout.json @@ -0,0 +1 @@ +[{"id":0,"type":"complication_background","background":"#background","goal_ring":{"is_enable":"#goal_ring","end_angle":"#fi","is_invert":"#$e"},"dimension":{"type":"rigid","width":"#size.w","height":"#size.h"},"placement":{"type":"absolute","left":"#pos.Ue","top":"#pos.Qe"},"visible":true,"inversion":false},{"id":1,"parent_id":0,"type":"complication_content","icon":"icBattery","text_low":"#ci","dimension":{"type":"rigid","width":76,"height":76},"placement":{"type":"relative"},"visible":true,"inversion":"#$e"},{"id":2,"parent_id":1,"type":"solid","placement":{"type":"absolute","left":29,"top":23},"color":"#nt","dimension":{"type":"rigid","height":6,"width":"#it"},"visible":true,"inversion":false},{"id":3,"parent_id":1,"type":"image","image_name":"icBattCharging","draw_mode":1,"placement":{"type":"absolute","left":34,"top":21},"dimension":{"width":6,"height":9},"visible":"#et","inversion":false}] diff --git a/app/src/main/assets/fossil_hr/complication_layout.json b/app/src/main/assets/fossil_hr/complication_layout.json new file mode 100644 index 000000000..42be572d2 --- /dev/null +++ b/app/src/main/assets/fossil_hr/complication_layout.json @@ -0,0 +1 @@ +[{"id":0,"type":"complication_background","background":"#background","goal_ring":{"is_enable":"#goal_ring","end_angle":"#fi","is_invert":"#$e"},"dimension":{"type":"rigid","width":"#size.w","height":"#size.h"},"placement":{"type":"absolute","left":"#pos.Ue","top":"#pos.Qe"},"visible":true,"inversion":false},{"id":1,"parent_id":0,"type":"complication_content","icon":"#icon","text_high":"#dt","text_low":"#ci","dimension":{"type":"rigid","width":76,"height":76},"placement":{"type":"relative"},"visible":true,"inversion":"#$e"}] \ No newline at end of file diff --git a/app/src/main/assets/fossil_hr/image_layout.json b/app/src/main/assets/fossil_hr/image_layout.json new file mode 100644 index 000000000..1ed732279 --- /dev/null +++ b/app/src/main/assets/fossil_hr/image_layout.json @@ -0,0 +1 @@ +[{"id":0,"type":"container","direction":1,"main_alignment":1,"cross_alignment":1,"dimension":{"type":"rigid","width":240,"height":240},"placement":{"type":"absolute","left":0,"top":0},"visible":true,"inversion":false},{"id":1,"parent_id":0,"type":"image","image_name":"#name","draw_mode":1,"placement":{"type":"absolute","left":"#pos.Ue","top":"#pos.Qe"},"dimension":{"width":"#size.w","height":"#size.h"},"visible":true,"inversion":false}] \ No newline at end of file diff --git a/app/src/main/assets/fossil_hr/menu_layout.json b/app/src/main/assets/fossil_hr/menu_layout.json new file mode 100644 index 000000000..6be3d627d --- /dev/null +++ b/app/src/main/assets/fossil_hr/menu_layout.json @@ -0,0 +1,207 @@ +[ + { + "id": 0, + "type": "container", + "direction": 1, + "main_alignment": 0, + "cross_alignment": 1, + "dimension": { + "type": "rigid", + "width": 240, + "height": 240 + }, + "placement": { + "type": "absolute", + "left": 0, + "top": 0 + }, + "visible": true, + "inversion": false + }, + { + "id": 1, + "parent_id": 0, + "type": "container", + "direction": 1, + "main_alignment": 1, + "cross_alignment": 2, + "dimension": { + "type": "rigid", + "width": 130, + "height": 34 + }, + "placement": { + "type": "absolute", + "left": 75, + "top": 45 + }, + "visible": true, + "inversion": false + }, + { + "id": 2, + "parent_id": 1, + "type": "text", + "text": "#top_short_press_label", + "ppem": 17, + "color": 3, + "placement": { + "type": "relative" + }, + "visible": true, + "inversion": false + }, + { + "id": 3, + "parent_id": 1, + "type": "text", + "text": "#top_long_press_label", + "ppem": 17, + "color": 3, + "ascent": 17, + "placement": { + "type": "relative" + }, + "visible": true, + "inversion": false + }, + { + "id": 4, + "parent_id": 0, + "type": "container", + "direction": 1, + "main_alignment": 1, + "cross_alignment": 2, + "dimension": { + "type": "rigid", + "width": 80, + "height": 34 + }, + "placement": { + "type": "absolute", + "left": 135, + "top": 103 + }, + "visible": true, + "inversion": false + }, + { + "id": 5, + "parent_id": 4, + "type": "text", + "text": "#middle_short_press_label", + "ppem": 17, + "color": 3, + "placement": { + "type": "relative" + }, + "visible": true, + "inversion": false + }, + { + "id": 6, + "parent_id": 4, + "type": "text", + "text": "#middle_long_press_label", + "ppem": 17, + "color": 3, + "ascent": 17, + "placement": { + "type": "relative" + }, + "visible": true, + "inversion": false + }, + { + "id": 7, + "parent_id": 0, + "type": "container", + "direction": 1, + "main_alignment": 1, + "cross_alignment": 2, + "dimension": { + "type": "rigid", + "width": 130, + "height": 34 + }, + "placement": { + "type": "absolute", + "left": 75, + "top": 161 + }, + "visible": true, + "inversion": false + }, + { + "id": 8, + "parent_id": 7, + "type": "text", + "text": "#bottom_short_press_label", + "ppem": 17, + "color": 3, + "placement": { + "type": "relative" + }, + "visible": true, + "inversion": false + }, + { + "id": 9, + "parent_id": 7, + "type": "text", + "text": "#bottom_long_press_label", + "ppem": 17, + "color": 3, + "ascent": 17, + "placement": { + "type": "relative" + }, + "visible": true, + "inversion": false + }, + { + "id": 10, + "parent_id": 0, + "type": "text", + "text": "#menu_title", + "ppem": 25, + "color": 3, + "ascent": 35, + "placement": { + "type": "relative" + }, + "visible": true, + "inversion": false + }, + { + "id": 11, + "parent_id": 0, + "type": "text_page", + "text": "#message_to_display", + "ppem": 17, + "color": 3, + "ascent": 17, + "cross_alignment": 2, + "line_width": [ + 80, + 85, + 90, + 90, + 85, + 80 + ], + "dimension": { + "type": "rigid", + "width": 80, + "height": 100 + }, + "placement": { + "type": "absolute", + "left": 20, + "top": 60 + }, + "visible": true, + "inversion": false + } + ] + \ No newline at end of file diff --git a/app/src/main/assets/fossil_hr/openSourceWatchface.bin b/app/src/main/assets/fossil_hr/openSourceWatchface.bin index e6cbe5917..41e239c15 100644 Binary files a/app/src/main/assets/fossil_hr/openSourceWatchface.bin and b/app/src/main/assets/fossil_hr/openSourceWatchface.bin differ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 1b95e6d4d..2425b7ec7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -62,11 +62,13 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper; +import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothStateChangeReceiver; +import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksContentObserver; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; @@ -143,6 +145,11 @@ public class GBApplication extends Application { private DeviceManager deviceManager; private BluetoothStateChangeReceiver bluetoothStateChangeReceiver; + private OpenTracksContentObserver openTracksObserver; + + private long lastAutoExportTimestamp = 0; + private long autoExportScheduledTimestamp = 0; + public static void quit() { GB.log("Quitting Gadgetbridge...", GB.INFO, null); Intent quitIntent = new Intent(GBApplication.ACTION_QUIT); @@ -210,6 +217,8 @@ public class GBApplication extends Application { loadAppsPebbleBlackList(); loadCalendarsBlackList(); + PeriodicExporter.enablePeriodicExport(context); + if (isRunningMarshmallowOrLater()) { notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (isRunningOreoOrLater()) { @@ -1090,4 +1099,28 @@ public class GBApplication extends Application { return "Gadgetbridge"; } } + + public void setOpenTracksObserver(OpenTracksContentObserver openTracksObserver) { + this.openTracksObserver = openTracksObserver; + } + + public OpenTracksContentObserver getOpenTracksObserver() { + return openTracksObserver; + } + + public long getLastAutoExportTimestamp() { + return lastAutoExportTimestamp; + } + + public void setLastAutoExportTimestamp(long lastAutoExportTimestamp) { + this.lastAutoExportTimestamp = lastAutoExportTimestamp; + } + + public long getAutoExportScheduledTimestamp() { + return autoExportScheduledTimestamp; + } + + public void setAutoExportScheduledTimestamp(long autoExportScheduledTimestamp) { + this.autoExportScheduledTimestamp = autoExportScheduledTimestamp; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DataManagementActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DataManagementActivity.java index 145a9f7c4..2b526cce8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DataManagementActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DataManagementActivity.java @@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.util.Date; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -51,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; @@ -145,7 +147,7 @@ public class DataManagementActivity extends AbstractGBActivity { cleanExportDirectory(); } }); - + GBApplication gbApp = GBApplication.app(); Prefs prefs = GBApplication.getPrefs(); boolean autoExportEnabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false); int autoExportInterval = prefs.getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0); @@ -153,16 +155,42 @@ public class DataManagementActivity extends AbstractGBActivity { //String autoExportLocation = prefs.getString(GBPrefs.AUTO_EXPORT_LOCATION, ""); int testExportVisibility = (autoExportInterval > 0 && autoExportEnabled) ? View.VISIBLE : View.GONE; - + boolean isExportEnabled = autoExportInterval > 0 && autoExportEnabled; TextView autoExportLocation_label = findViewById(R.id.autoExportLocation_label); autoExportLocation_label.setVisibility(testExportVisibility); - TextView autoExportLocation_intro = findViewById(R.id.autoExportLocation_intro); - autoExportLocation_intro.setVisibility(testExportVisibility); - TextView autoExportLocation_path = findViewById(R.id.autoExportLocation_path); autoExportLocation_path.setVisibility(testExportVisibility); - autoExportLocation_path.setText(getAutoExportLocationSummary()); + autoExportLocation_path.setText(getAutoExportLocationUserString() + " (" + getAutoExportLocationPreferenceString() + ")" ); + + TextView autoExportEnabled_label = findViewById(R.id.autoExportEnabled); + if (isExportEnabled) { + autoExportEnabled_label.setText(getString(R.string.activity_db_management_autoexport_enabled_yes)); + } else { + autoExportEnabled_label.setText(getString(R.string.activity_db_management_autoexport_enabled_no)); + } + + TextView autoExportScheduled = findViewById(R.id.autoExportScheduled); + autoExportScheduled.setVisibility(testExportVisibility); + long setAutoExportScheduledTimestamp = gbApp.getAutoExportScheduledTimestamp(); + if (setAutoExportScheduledTimestamp > 0) { + autoExportScheduled.setText(getString(R.string.activity_db_management_autoexport_scheduled_yes, + DateTimeUtils.formatDateTime(new Date(setAutoExportScheduledTimestamp)))); + } else { + autoExportScheduled.setText(getResources().getString(R.string.activity_db_management_autoexport_scheduled_no)); + } + + TextView autoExport_lastTime_label = findViewById(R.id.autoExport_lastTime_label); + long lastAutoExportTimestamp = gbApp.getLastAutoExportTimestamp(); + + autoExport_lastTime_label.setVisibility(View.GONE); + autoExport_lastTime_label.setText(getString(R.string.autoExport_lastTime_label, + DateTimeUtils.formatDateTime(new Date(lastAutoExportTimestamp)))); + + if (lastAutoExportTimestamp > 0) { + autoExport_lastTime_label.setVisibility(testExportVisibility); + autoExport_lastTime_label.setVisibility(testExportVisibility); + } final Context context = getApplicationContext(); Button testExportDBButton = findViewById(R.id.testExportDBButton); @@ -180,18 +208,25 @@ public class DataManagementActivity extends AbstractGBActivity { sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); } - - - //would rather re-use method of SettingsActivity... but lifecycle... - private String getAutoExportLocationSummary() { + private String getAutoExportLocationPreferenceString() { String autoExportLocation = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null); if (autoExportLocation == null) { return ""; } + return autoExportLocation; + } + + private String getAutoExportLocationUri() { + String autoExportLocation = getAutoExportLocationPreferenceString(); + if (autoExportLocation == null) { + return ""; + } Uri uri = Uri.parse(autoExportLocation); try { + return AndroidUtils.getFilePath(getApplicationContext(), uri); } catch (IllegalArgumentException e) { + LOG.error("getFilePath did not work, trying to resolve content provider path"); try { Cursor cursor = getContentResolver().query( uri, @@ -208,6 +243,13 @@ public class DataManagementActivity extends AbstractGBActivity { return ""; } + private String getAutoExportLocationUserString() { + String location = getAutoExportLocationUri(); + if (location == "") { + return getString(R.string.activity_db_management_autoexport_location); + } + return location; + } private boolean hasOldActivityDatabase() { return new DBHelper(this).existsDB("ActivityDatabase"); @@ -403,7 +445,7 @@ public class DataManagementActivity extends AbstractGBActivity { public void onClick(DialogInterface dialog, int which) { try { File externalFilesDir = FileUtils.getExternalFilesDir(); - String autoexportFile = getAutoExportLocationSummary(); + String autoexportFile = getAutoExportLocationUri(); for (File file : externalFilesDir.listFiles()) { if (file.isFile() && (!FileUtils.getExtension(file.toString()).toLowerCase().equals("gpx")) && //keep GPX files diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index 38bdc1047..29957ada4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -35,6 +35,7 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -70,6 +71,8 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Random; +import java.util.Timer; +import java.util.TimerTask; import java.util.TreeMap; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -83,6 +86,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksContentObserver; +import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -508,6 +513,72 @@ public class DebugActivity extends AbstractGBActivity { } }); + Button startFitnessAppTracking = findViewById(R.id.startFitnessAppTracking); + startFitnessAppTracking.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + OpenTracksController.startRecording(DebugActivity.this); + } + }); + + Button stopFitnessAppTracking = findViewById(R.id.stopFitnessAppTracking); + stopFitnessAppTracking.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + OpenTracksController.stopRecording(DebugActivity.this); + } + }); + + Button showStatusFitnessAppTracking = findViewById(R.id.showStatusFitnessAppTracking); + final int delay = 2 * 1000; + + showStatusFitnessAppTracking.setOnClickListener(new View.OnClickListener() { + final Handler handler = new Handler(); + Runnable runnable; + + @Override + public void onClick(View v) { + final AlertDialog.Builder fitnesStatusBuilder = new AlertDialog.Builder(DebugActivity.this); + fitnesStatusBuilder + .setCancelable(false) + .setTitle("openTracksObserver Status") + .setMessage("Starting openTracksObserver watcher, waiting for an update, refreshing every: " + delay + "ms") + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + handler.removeCallbacks(runnable); + } + }); + final AlertDialog alert = fitnesStatusBuilder.show(); + + + runnable = new Runnable() { + @Override + public void run() { + LOG.debug("openTracksObserver debug watch dialog running"); + handler.postDelayed(this, delay); //schedule next execution + + OpenTracksContentObserver openTracksObserver = GBApplication.app().getOpenTracksObserver(); + if (openTracksObserver == null) { + LOG.debug("openTracksObserver is null"); + alert.cancel(); + alert.setMessage("openTracksObserver not running"); + alert.show(); + return; + } + LOG.debug("openTracksObserver is not null, updating debug view"); + long timeSecs = openTracksObserver.getTimeMillisChange() / 1000; + float distanceCM = openTracksObserver.getDistanceMeterChange() * 100; + + LOG.debug("Time: " + timeSecs + " distanceCM " + distanceCM); + alert.cancel(); + alert.setMessage("TimeSec: " + timeSecs + " distanceCM " + distanceCM); + alert.show(); + } + }; + handler.postDelayed(runnable, delay); + } + }); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationManagementActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationManagementActivity.java index a007fb5d2..a585eb0f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationManagementActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationManagementActivity.java @@ -42,13 +42,13 @@ import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceManager; +import android.provider.Settings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -108,11 +108,31 @@ public class NotificationManagementActivity extends AbstractSettingsActivity { category.removePreference(pref); } + pref = findPreference("notifications_settings"); + pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + //This could open notification channel settings, if needed...: + //Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + //intent.putExtra(Settings.EXTRA_CHANNEL_ID, GB.NOTIFICATION_CHANNEL_ID_TRANSFER); + startActivity(intent); + return true; + } + }); + if (GBApplication.isRunningTenOrLater()) { pref = findPreference("minimize_priority"); PreferenceCategory category = (PreferenceCategory) findPreference("pref_key_notifications"); category.removePreference(pref); } + + if (!GBApplication.isRunningOreoOrLater()) { + pref = findPreference("notifications_settings"); + PreferenceCategory category = (PreferenceCategory) findPreference("pref_key_notifications"); + category.removePreference(pref); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index b8a590671..5970cc691 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -19,8 +19,11 @@ package nodomain.freeyourgadget.gadgetbridge.activities; import android.Manifest; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; @@ -28,16 +31,19 @@ import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; -import android.media.Ringtone; -import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.PreferenceCategory; import android.preference.PreferenceManager; import android.provider.DocumentsContract; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; import android.widget.Toast; import androidx.core.app.ActivityCompat; @@ -73,7 +79,8 @@ public class SettingsActivity extends AbstractSettingsActivity { public static final String PREF_MEASUREMENT_SYSTEM = "measurement_system"; private static final int FILE_REQUEST_CODE = 4711; - + private EditText fitnessAppEditText = null; + private int fitnessAppSelectionListSpinnerFirstRun = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -306,7 +313,7 @@ public class SettingsActivity extends AbstractSettingsActivity { Integer.valueOf((String) autoExportInterval)); preference.setSummary(summary); boolean auto_export_enabled = GBApplication.getPrefs().getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false); - PeriodicExporter.sheduleAlarm(getApplicationContext(), Integer.valueOf((String) autoExportInterval), auto_export_enabled); + PeriodicExporter.scheduleAlarm(getApplicationContext(), Integer.valueOf((String) autoExportInterval), auto_export_enabled); return true; } }); @@ -320,7 +327,7 @@ public class SettingsActivity extends AbstractSettingsActivity { @Override public boolean onPreferenceChange(Preference preference, Object autoExportEnabled) { int autoExportInterval = GBApplication.getPrefs().getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0); - PeriodicExporter.sheduleAlarm(getApplicationContext(), autoExportInterval, (boolean) autoExportEnabled); + PeriodicExporter.scheduleAlarm(getApplicationContext(), autoExportInterval, (boolean) autoExportEnabled); return true; } }); @@ -403,6 +410,70 @@ public class SettingsActivity extends AbstractSettingsActivity { } }); + //fitness app (OpenTracks) package name selection for OpenTracks observer + pref = findPreference("pref_key_opentracks_packagename"); + pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + + final LinearLayout outerLayout = new LinearLayout(SettingsActivity.this); + outerLayout.setOrientation(LinearLayout.VERTICAL); + final LinearLayout innerLayout = new LinearLayout(SettingsActivity.this); + innerLayout.setOrientation(LinearLayout.HORIZONTAL); + innerLayout.setPadding(20, 0, 20, 0); + final Spinner selectionListSpinner = new Spinner(SettingsActivity.this); + String[] appListArray = getResources().getStringArray(R.array.fitness_tracking_apps_package_names); + ArrayAdapter spinnerArrayAdapter = new ArrayAdapter(SettingsActivity.this, + android.R.layout.simple_spinner_dropdown_item, appListArray); + selectionListSpinner.setAdapter(spinnerArrayAdapter); + fitnessAppSelectionListSpinnerFirstRun = 0; + addListenerOnSpinnerDeviceSelection(selectionListSpinner); + Prefs prefs = GBApplication.getPrefs(); + String packageName = prefs.getString("opentracks_packagename", "de.dennisguse.opentracks"); + fitnessAppEditText = new EditText(SettingsActivity.this); + fitnessAppEditText.setText(packageName); + innerLayout.addView(fitnessAppEditText); + outerLayout.addView(selectionListSpinner); + outerLayout.addView(innerLayout); + + new AlertDialog.Builder(SettingsActivity.this) + .setCancelable(true) + .setTitle(R.string.pref_title_opentracks_packagename) + .setView(outerLayout) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit(); + editor.putString("opentracks_packagename", fitnessAppEditText.getText().toString()); + editor.apply(); + editor.commit(); + } + }) + .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .show(); + return false; + } + }); + } + + private void addListenerOnSpinnerDeviceSelection(Spinner spinner) { + spinner.setOnItemSelectedListener(new SettingsActivity.CustomOnDeviceSelectedListener()); + } + + public class CustomOnDeviceSelectedListener implements AdapterView.OnItemSelectedListener { + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + if (++fitnessAppSelectionListSpinnerFirstRun > 1) { //this prevents the setText to be set when spinner just is being initialized + fitnessAppEditText.setText(parent.getItemAtPosition(pos).toString()); + } + } + + @Override + public void onNothingSelected(AdapterView arg0) { + // TODO Auto-generated method stub + } } @Override @@ -421,7 +492,7 @@ public class SettingsActivity extends AbstractSettingsActivity { .getPrefs().getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false); int autoExportPeriod = GBApplication .getPrefs().getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0); - PeriodicExporter.sheduleAlarm(getApplicationContext(), autoExportPeriod, autoExportEnabled); + PeriodicExporter.scheduleAlarm(getApplicationContext(), autoExportPeriod, autoExportEnabled); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/PeriodicExporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/PeriodicExporter.java index 2fe98665e..e3d5e3568 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/PeriodicExporter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/PeriodicExporter.java @@ -44,24 +44,30 @@ public class PeriodicExporter extends BroadcastReceiver { public static void enablePeriodicExport(Context context) { Prefs prefs = GBApplication.getPrefs(); + GBApplication gbApp = GBApplication.app(); + long autoExportScheduled = gbApp.getAutoExportScheduledTimestamp(); boolean autoExportEnabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false); Integer autoExportInterval = prefs.getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0); - sheduleAlarm(context, autoExportInterval, autoExportEnabled); + scheduleAlarm(context, autoExportInterval, autoExportEnabled && autoExportScheduled == 0); } - public static void sheduleAlarm(Context context, Integer autoExportInterval, boolean autoExportEnabled) { + public static void scheduleAlarm(Context context, Integer autoExportInterval, boolean autoExportEnabled) { Intent i = new Intent(context, PeriodicExporter.class); - PendingIntent pi = PendingIntent.getBroadcast(context, 0 , i, 0); + PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, 0); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); am.cancel(pi); if (!autoExportEnabled) { + LOG.info("Not scheduling periodic export, either already scheduled or not enabled"); return; } int exportPeriod = autoExportInterval * 60 * 60 * 1000; if (exportPeriod == 0) { + LOG.info("Not scheduling periodic export, interval set to 0"); return; } - LOG.info("Enabling periodic export"); + LOG.info("Scheduling periodic export"); + GBApplication gbApp = GBApplication.app(); + gbApp.setAutoExportScheduledTimestamp(System.currentTimeMillis() + exportPeriod); am.setInexactRepeating( AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + exportPeriod, @@ -72,21 +78,46 @@ public class PeriodicExporter extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - LOG.info("Exporting DB"); - try (DBHandler dbHandler = GBApplication.acquireDB()) { - DBHelper helper = new DBHelper(context); - String dst = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null); - if (dst == null) { - LOG.info("Unable to export DB, export location not set"); - return; + LOG.info("Received command to export DB"); + createRefreshTask("Export database", context).execute(); + } + + protected RefreshTask createRefreshTask(String task, Context context) { + return new RefreshTask(task, context); + } + + public class RefreshTask extends DBAccess { + Context localContext; + + public RefreshTask(String task, Context context) { + super(task, context); + localContext = context; + } + + @Override + protected void doInBackground(DBHandler handler) { + LOG.info("Exporting DB in a background thread"); + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DBHelper helper = new DBHelper(localContext); + String dst = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null); + if (dst == null) { + LOG.info("Unable to export DB, export location not set"); + return; + } + Uri dstUri = Uri.parse(dst); + try (OutputStream out = localContext.getContentResolver().openOutputStream(dstUri)) { + helper.exportDB(dbHandler, out); + GBApplication gbApp = GBApplication.app(); + gbApp.setLastAutoExportTimestamp(System.currentTimeMillis()); + } + } catch (Exception ex) { + GB.updateExportFailedNotification(localContext.getString(R.string.notif_export_failed_title), localContext); + LOG.info("Exception while exporting DB: ", ex); } - Uri dstUri = Uri.parse(dst); - try (OutputStream out = context.getContentResolver().openOutputStream(dstUri)) { - helper.exportDB(dbHandler, out); - } - } catch (Exception ex) { - GB.updateExportFailedNotification(context.getString(R.string.notif_export_failed_title), context); - LOG.info("Exception while exporting DB: ", ex); + } + + @Override + protected void onPostExecute(Object o) { } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java index 8195c76e5..3f9a0911a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java @@ -63,7 +63,8 @@ public class FitProDeviceCoordinator extends AbstractDeviceCoordinator { if (name != null && ( name.startsWith("M6") || name.startsWith("M4") || - name.equals("LH716")) + name.equals("LH716") || + name.equals("Fit1900")) ) { return DeviceType.FITPRO; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java index d57ae3816..d9cdc70b0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java @@ -80,6 +80,8 @@ public class HuamiConst { public static final String PREF_BUTTON_ACTION_BROADCAST = "button_action_broadcast"; public static final String PREF_BUTTON_ACTION_SELECTION_OFF = "UNKNOWN"; public static final String PREF_BUTTON_ACTION_SELECTION_BROADCAST = "BROADCAST"; + public static final String PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_START = "FITNESS_CONTROL_START"; + public static final String PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_STOP = "FITNESS_CONTROL_STOP"; public static final String PREF_DEVICE_ACTION_SELECTION_OFF = "UNKNOWN"; public static final String PREF_DEVICE_ACTION_SELECTION_BROADCAST = "BROADCAST"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilAppWriter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilAppWriter.java index fc8b199c7..4917e879a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilAppWriter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilAppWriter.java @@ -39,11 +39,11 @@ public class FossilAppWriter { private String version; private LinkedHashMap code; private LinkedHashMap icons; - private LinkedHashMap layout; + private LinkedHashMap layout; private LinkedHashMap displayName; private LinkedHashMap config; - public FossilAppWriter(Context context, String version, LinkedHashMap code, LinkedHashMap icons, LinkedHashMap layout, LinkedHashMap displayName, LinkedHashMap config) { + public FossilAppWriter(Context context, String version, LinkedHashMap code, LinkedHashMap icons, LinkedHashMap layout, LinkedHashMap displayName, LinkedHashMap config) { this.mContext = context; if (this.mContext == null) throw new AssertionError("context cannot be null"); this.version = version; @@ -61,9 +61,9 @@ public class FossilAppWriter { } public byte[] getWapp() throws IOException { - byte[] codeData = loadFiles(code); - byte[] iconsData = loadFiles(icons); - byte[] layoutData = loadStringFiles(layout); + byte[] codeData = loadFiles(code, false); + byte[] iconsData = loadFiles(icons, false); + byte[] layoutData = loadFiles(layout, true); byte[] displayNameData = loadStringFiles(displayName); byte[] configData = loadStringFiles(config); @@ -118,16 +118,23 @@ public class FossilAppWriter { return wapp.toByteArray(); } - public byte[] loadFiles(LinkedHashMap filesMap) throws IOException { + public byte[] loadFiles(LinkedHashMap filesMap, boolean appendNull) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); for (String filename : filesMap.keySet()) { InputStream in = filesMap.get(filename); output.write((byte)filename.length() + 1); output.write(StringUtils.terminateNull(filename).getBytes(StandardCharsets.UTF_8)); - output.write(shortToLEBytes((short)in.available())); + int fileLength = in.available(); + if(appendNull){ + fileLength++; + } + output.write(shortToLEBytes((short)fileLength)); byte[] fileBytes = new byte[in.available()]; in.read(fileBytes); output.write(fileBytes); + if(appendNull){ + output.write(0x00); + } } return output.toByteArray(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java index 0a3b5cd87..3464ca3a5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java @@ -456,6 +456,9 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem if (watchfaceConfig.has("powersave_hands")) { watchfaceSettings.setPowersaveHands(watchfaceConfig.getBoolean("powersave_hands")); } + if (watchfaceConfig.has("light_up_on_notification")) { + watchfaceSettings.setLightUpOnNotification(watchfaceConfig.getBoolean("light_up_on_notification")); + } } catch (JSONException e) { LOG.warn("JSON parsing error", e); } @@ -576,39 +579,46 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem if (widget != null) { posY.setText(Integer.toString(widget.getPosY())); } - // Configure position preset buttons - Button btnTop = layout.findViewById(R.id.watchface_widget_preset_top); - btnTop.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - posX.setText("120"); - posY.setText("58"); + + class WidgetPosition{ + final int posX, posY, buttonResource, hintStringResource; + + public WidgetPosition(int posX, int posY, int buttonResource, int hintStringResource) { + this.posX = posX; + this.posY = posY; + this.buttonResource = buttonResource; + this.hintStringResource = hintStringResource; } - }); - Button btnBottom = layout.findViewById(R.id.watchface_widget_preset_bottom); - btnBottom.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - posX.setText("120"); - posY.setText("182"); + } + + WidgetPosition[] positions = new WidgetPosition[]{ + new WidgetPosition(120, 58, R.id.watchface_widget_preset_top, R.string.watchface_dialog_widget_preset_top), + new WidgetPosition(182, 120, R.id.watchface_widget_preset_right, R.string.watchface_dialog_widget_preset_right), + new WidgetPosition(120, 182, R.id.watchface_widget_preset_bottom, R.string.watchface_dialog_widget_preset_bottom), + new WidgetPosition(58, 120, R.id.watchface_widget_preset_left, R.string.watchface_dialog_widget_preset_left), + }; + + for(final WidgetPosition position : positions){ + Button btn = layout.findViewById(position.buttonResource); + btn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + posX.setText(String.valueOf(position.posX)); + posY.setText(String.valueOf(position.posY)); + } + }); + } + + if(widget == null){ + int currentIndex = widgets.size(); + if(currentIndex < 4){ + WidgetPosition newPosition = positions[currentIndex]; + posX.setText(String.valueOf(newPosition.posX)); + posY.setText(String.valueOf(newPosition.posY)); + GB.toast(getString(R.string.watchface_dialog_pre_setting_position, getString(newPosition.hintStringResource)), Toast.LENGTH_SHORT, GB.INFO); } - }); - Button btnLeft = layout.findViewById(R.id.watchface_widget_preset_left); - btnLeft.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - posX.setText("58"); - posY.setText("120"); - } - }); - Button btnRight = layout.findViewById(R.id.watchface_widget_preset_right); - btnRight.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - posX.setText("182"); - posY.setText("120"); - } - }); + } + // Set widget size final LinearLayout sizeLayout = layout.findViewById(R.id.watchface_widget_size_layout); sizeLayout.setVisibility(View.GONE); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceFactory.java index b0cdfd227..be473880a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceFactory.java @@ -185,22 +185,15 @@ public class HybridHRWatchfaceFactory { } catch (IOException e) { LOG.warn("Unable to read asset file", e); } - LinkedHashMap layout = new LinkedHashMap<>(); - try { - layout.put("complication_layout", getComplicationLayout()); - } catch (JSONException e) { - LOG.warn("Could not generate complication_layout", e); - } - try { - layout.put("image_layout", getImageLayout()); - } catch (JSONException e) { - LOG.warn("Could not generate image_layout", e); - } - try { - if (includeWidget("widgetBattery") > 0) layout.put("battery_layout", getBatteryLayout()); - } catch (JSONException e) { - LOG.warn("Could not generate battery_layout", e); + LinkedHashMap layout = new LinkedHashMap<>(); + layout.put("complication_layout", context.getAssets().open("fossil_hr/complication_layout.json")); + layout.put("image_layout", context.getAssets().open("fossil_hr/image_layout.json")); + layout.put("menu_layout", context.getAssets().open("fossil_hr/menu_layout.json")); + + if (includeWidget("widgetBattery") > 0) { + layout.put("battery_layout", context.getAssets().open("fossil_hr/battery_layout.json")); } + LinkedHashMap displayName = new LinkedHashMap<>(); displayName.put("display_name", watchfaceName); displayName.put("theme_class", "complications"); @@ -214,184 +207,6 @@ public class HybridHRWatchfaceFactory { return appWriter.getWapp(); } - private String getBatteryLayout() throws JSONException { - JSONArray batteryLayout = new JSONArray(); - - JSONObject complicationBackground = new JSONObject(); - complicationBackground.put("id", 0); - complicationBackground.put("type", "complication_background"); - complicationBackground.put("background", "#background"); - complicationBackground.put("visible", true); - complicationBackground.put("inversion", false); - JSONObject goalRing = new JSONObject(); - goalRing.put("is_enable", "#goal_ring"); - goalRing.put("end_angle", "#fi"); - goalRing.put("is_invert", "#$e"); - complicationBackground.put("goal_ring", goalRing); - JSONObject dimension = new JSONObject(); - dimension.put("type", "rigid"); - dimension.put("width", "#size.w"); - dimension.put("height", "#size.h"); - complicationBackground.put("dimension", dimension); - JSONObject placement = new JSONObject(); - placement.put("type", "absolute"); - placement.put("left", "#pos.Ue"); - placement.put("top", "#pos.Qe"); - complicationBackground.put("placement", placement); - batteryLayout.put(complicationBackground); - - JSONObject complicationContent = new JSONObject(); - complicationContent.put("id", 1); - complicationContent.put("parent_id", 0); - complicationContent.put("type", "complication_content"); - complicationContent.put("icon", "icBattery"); - complicationContent.put("text_low", "#ci"); - complicationContent.put("visible", true); - complicationContent.put("inversion", "#$e"); - dimension = new JSONObject(); - dimension.put("type", "rigid"); - dimension.put("width", 76); - dimension.put("height", 76); - complicationContent.put("dimension", dimension); - placement = new JSONObject(); - placement.put("type", "relative"); - complicationContent.put("placement", placement); - batteryLayout.put(complicationContent); - - JSONObject chargingStatus = new JSONObject(); - chargingStatus.put("id", 2); - chargingStatus.put("parent_id", 1); - chargingStatus.put("type", "solid"); - chargingStatus.put("color", "#nt"); - chargingStatus.put("visible", true); - chargingStatus.put("inversion", false); - dimension = new JSONObject(); - dimension.put("type", "rigid"); - dimension.put("width", "#it"); - dimension.put("height", 6); - chargingStatus.put("dimension", dimension); - placement = new JSONObject(); - placement.put("type", "absolute"); - placement.put("left", 29); - placement.put("top", 23); - chargingStatus.put("placement", placement); - batteryLayout.put(chargingStatus); - - JSONObject image = new JSONObject(); - image.put("id", 3); - image.put("parent_id", 1); - image.put("type", "image"); - image.put("image_name", "icBattCharging"); - image.put("draw_mode", 1); - image.put("visible", "#et"); - image.put("inversion", false); - placement = new JSONObject(); - placement.put("type", "absolute"); - placement.put("left", 34); - placement.put("top", 21); - image.put("placement", placement); - dimension = new JSONObject(); - dimension.put("width", 6); - dimension.put("height", 9); - image.put("dimension", dimension); - batteryLayout.put(image); - - return batteryLayout.toString(); - } - - private String getComplicationLayout() throws JSONException { - JSONArray complicationLayout = new JSONArray(); - - JSONObject complicationBackground = new JSONObject(); - complicationBackground.put("id", 0); - complicationBackground.put("type", "complication_background"); - complicationBackground.put("background", "#background"); - complicationBackground.put("visible", true); - complicationBackground.put("inversion", false); - JSONObject goalRing = new JSONObject(); - goalRing.put("is_enable", "#goal_ring"); - goalRing.put("end_angle", "#fi"); - goalRing.put("is_invert", "#$e"); - complicationBackground.put("goal_ring", goalRing); - JSONObject dimension = new JSONObject(); - dimension.put("type", "rigid"); - dimension.put("width", "#size.w"); - dimension.put("height", "#size.h"); - complicationBackground.put("dimension", dimension); - JSONObject placement = new JSONObject(); - placement.put("type", "absolute"); - placement.put("left", "#pos.Ue"); - placement.put("top", "#pos.Qe"); - complicationBackground.put("placement", placement); - complicationLayout.put(complicationBackground); - - JSONObject complicationContent = new JSONObject(); - complicationContent.put("id", 1); - complicationContent.put("parent_id", 0); - complicationContent.put("type", "complication_content"); - complicationContent.put("icon", "#icon"); - complicationContent.put("text_high", "#dt"); - complicationContent.put("text_low", "#ci"); - complicationContent.put("visible", true); - complicationContent.put("inversion", "#$e"); - dimension = new JSONObject(); - dimension.put("type", "rigid"); - dimension.put("width", "#size.w"); - dimension.put("height", "#size.h"); - complicationContent.put("dimension", dimension); - placement = new JSONObject(); - placement.put("type", "relative"); - complicationContent.put("placement", placement); - complicationLayout.put(complicationContent); - - return complicationLayout.toString(); - } - - private String getImageLayout() throws JSONException { - JSONArray imageLayout = new JSONArray(); - - JSONObject container = new JSONObject(); - container.put("id", 0); - container.put("type", "container"); - container.put("direction", 1); - container.put("main_alignment", 1); - container.put("cross_alignment", 1); - container.put("visible", true); - container.put("inversion", false); - JSONObject dimension = new JSONObject(); - dimension.put("type", "rigid"); - dimension.put("width", 240); - dimension.put("height", 240); - container.put("dimension", dimension); - JSONObject placement = new JSONObject(); - placement.put("type", "absolute"); - placement.put("left", 0); - placement.put("top", 0); - container.put("placement", placement); - imageLayout.put(container); - - JSONObject image = new JSONObject(); - image.put("id", 1); - image.put("parent_id", 0); - image.put("type", "image"); - image.put("image_name", "#name"); - image.put("draw_mode", 1); - image.put("visible", true); - image.put("inversion", false); - placement = new JSONObject(); - placement.put("type", "absolute"); - placement.put("left", "#pos.Ue"); - placement.put("top", "#pos.Qe"); - image.put("placement", placement); - dimension = new JSONObject(); - dimension.put("width", "#size.w"); - dimension.put("height", "#size.h"); - image.put("dimension", dimension); - imageLayout.put(image); - - return imageLayout.toString(); - } - private String getConfiguration() throws JSONException { JSONObject configuration = new JSONObject(); @@ -432,6 +247,7 @@ public class HybridHRWatchfaceFactory { config.put("wrist_flick_duration", settings.getWristFlickDuration()); config.put("wrist_flick_move_hour", settings.getWristFlickMoveHour()); config.put("wrist_flick_move_minute", settings.getWristFlickMoveMinute()); + config.put("light_up_on_notification", settings.getLightUpOnNotification()); config.put("powersave_display", settings.getPowersaveDisplay()); config.put("powersave_hands", settings.getPowersaveHands()); configuration.put("config", config); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettings.java index cd402da3e..ef3ebe074 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettings.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettings.java @@ -27,6 +27,7 @@ public class HybridHRWatchfaceSettings implements Serializable { private int wristFlickMoveMinute = -360; private boolean powersaveDisplay = false; private boolean powersaveHands = false; + private boolean lightUpOnNotification = false; public HybridHRWatchfaceSettings() { } @@ -63,6 +64,14 @@ public class HybridHRWatchfaceSettings implements Serializable { this.wristFlickDuration = wristFlickDuration; } + public boolean getLightUpOnNotification() { + return lightUpOnNotification; + } + + public void setLightUpOnNotification(boolean lightUpOnNotification) { + this.lightUpOnNotification = lightUpOnNotification; + } + public int getWristFlickMoveHour() { return wristFlickMoveHour; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettingsActivity.java index 2fb1a9725..50485eb99 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceSettingsActivity.java @@ -111,6 +111,10 @@ public class HybridHRWatchfaceSettingsActivity extends AbstractSettingsActivity SwitchPreference power_saving_hands = (SwitchPreference) findPreference("pref_hybridhr_watchface_power_saving_hands"); power_saving_hands.setOnPreferenceChangeListener(new PreferenceChangeListener()); power_saving_hands.setChecked(settings.getPowersaveHands()); + + SwitchPreference light_up_on_notification = (SwitchPreference) findPreference("pref_hybridhr_watchface_light_up_on_notification"); + light_up_on_notification.setOnPreferenceChangeListener(new PreferenceChangeListener()); + light_up_on_notification.setChecked(settings.getLightUpOnNotification()); } private static class PreferenceChangeListener implements Preference.OnPreferenceChangeListener { @@ -136,6 +140,9 @@ public class HybridHRWatchfaceSettingsActivity extends AbstractSettingsActivity settings.setWristFlickMoveMinute(Integer.parseInt(newValue.toString())); preference.setSummary(newValue.toString()); break; + case "pref_hybridhr_watchface_light_up_on_notification": + settings.setLightUpOnNotification((boolean) newValue); + break; case "pref_hybridhr_watchface_wrist_flick_duration": settings.setWristFlickDuration(Integer.parseInt(newValue.toString())); preference.setSummary(newValue.toString()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/um25/Activity/DataActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/um25/Activity/DataActivity.java index 1b4cee1ed..485398cdf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/um25/Activity/DataActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/um25/Activity/DataActivity.java @@ -5,17 +5,21 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; +import android.view.View; import android.widget.TextView; +import android.widget.Toast; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import java.lang.reflect.Field; import java.util.HashMap; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Data.MeasurementData; import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support.UM25Support; +import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DataActivity extends AbstractGBActivity { private HashMap valueViews = new HashMap<>(ValueDisplay.values().length); @@ -51,6 +55,21 @@ public class DataActivity extends AbstractGBActivity { setContentView(R.layout.activity_um25_data); chargeDurationTextView = findViewById(R.id.um25_text_charge_duration); + TextView wattHoursTextView = findViewById(R.id.um25_text_wattage_sum); + + View.OnLongClickListener longClickListener = new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + GB.toast("resetting", Toast.LENGTH_SHORT, GB.INFO); + LocalBroadcastManager.getInstance(DataActivity.this).sendBroadcast( + new Intent(UM25Support.ACTION_RESET_STATS) + ); + return true; + } + }; + + chargeDurationTextView.setOnLongClickListener(longClickListener); + wattHoursTextView.setOnLongClickListener(longClickListener); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescControlActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescControlActivity.java index d4d9de44f..8c745f975 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescControlActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescControlActivity.java @@ -72,8 +72,8 @@ public class VescControlActivity extends AbstractGBActivity { } private void restoreValues(){ - rpmEditText.setText(preferences.getInt(PREFS_KEY_LAST_RPM, 0)); - breakCurrentEditText.setText(preferences.getInt(PREFS_KEY_LAST_BREAK_CURRENT, 0)); + rpmEditText.setText(String.valueOf(preferences.getInt(PREFS_KEY_LAST_RPM, 0))); + breakCurrentEditText.setText(String.valueOf(preferences.getInt(PREFS_KEY_LAST_BREAK_CURRENT, 0))); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchService.java index 40c298175..d7a80c589 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchService.java @@ -29,6 +29,9 @@ public class XWatchService { public static final byte COMMAND_ACTION_BUTTON = 0x4c; public static final byte COMMAND_ACTIVITY_DATA = 0x43; public static final byte COMMAND_ACTIVITY_TOTALS = 0x46; + public static final byte COMMAND_NOTIFICATION = 0x4d; + public static final byte COMMAND_NOTIFICATION_PHONE = 0x00; + public static final byte COMMAND_NOTIFICATION_MESSAGE = 0x01; private static final Map XWATCH_DEBUG; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AutoStartReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AutoStartReceiver.java index 208b90b0f..f5e916486 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AutoStartReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AutoStartReceiver.java @@ -38,7 +38,7 @@ public class AutoStartReceiver extends BroadcastReceiver { } else { GBApplication.deviceService().start(); } - + Log.i(TAG, "Going to enable periodic exporter"); PeriodicExporter.enablePeriodicExport(context); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index 3bab231c7..1402a92d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -881,6 +881,7 @@ public class NotificationListener extends NotificationListenerService { String source = sbn.getPackageName(); if (source.equals("de.dennisguse.opentracks") || source.equals("de.dennisguse.opentracks.debug") + || source.equals("de.dennisguse.opentracks.nightly") || source.equals("de.tadris.fitness") || source.equals("de.tadris.fitness.debug") ) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksContentObserver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksContentObserver.java new file mode 100644 index 000000000..aee21b9da --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksContentObserver.java @@ -0,0 +1,393 @@ +/* Copyright (C) 2022 Arjan Schrijver + + 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 . */ + +package nodomain.freeyourgadget.gadgetbridge.externalevents; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + + +public class OpenTracksContentObserver extends ContentObserver { + private Context mContext; + private Uri tracksUri; + private int protocolVersion; + private int totalTimeMillis; + private float totalDistanceMeter; + + private long previousTimeMillis = 0; + private float previousDistanceMeter = 0; + + public int getTotalTimeMillis() { + return totalTimeMillis; + } + public float getTotalDistanceMeter() { + return totalDistanceMeter; + } + + public long getTimeMillisChange() { + /** + * We don't use the timeMillis received from OpenTracks here, because those updates do not + * come in very regularly when GPS reception is bad + */ + long timeMillisDelta = System.currentTimeMillis() - previousTimeMillis; + previousTimeMillis = System.currentTimeMillis(); + return timeMillisDelta; + } + + public float getDistanceMeterChange() { + float distanceMeterDelta = totalDistanceMeter - previousDistanceMeter; + previousDistanceMeter = totalDistanceMeter; + return distanceMeterDelta; + } + + + public OpenTracksContentObserver(Context context, final Uri tracksUri, final int protocolVersion) { + super(new Handler()); + this.mContext = context; + this.tracksUri = tracksUri; + this.protocolVersion = protocolVersion; + this.previousTimeMillis = System.currentTimeMillis(); + } + + @Override + public void onChange(final boolean selfChange, final Uri uri) { + if (uri == null) { + return; // nothing can be done without an uri + } + if (tracksUri.toString().startsWith(uri.toString())) { + final List tracks = Track.readTracks(mContext.getContentResolver(), tracksUri, protocolVersion); + if (!tracks.isEmpty()) { + final TrackStatistics statistics = new TrackStatistics(tracks); + totalTimeMillis = statistics.getTotalTimeMillis(); + totalDistanceMeter = statistics.getTotalDistanceMeter(); + } + } + } + + public void unregister() { + if (mContext != null) { + mContext.getContentResolver().unregisterContentObserver(this); + } + } + + public void finish() { + unregister(); + if (mContext != null) { + ((Activity) mContext).finish(); + mContext = null; + } + } +} + +class Track { + /** + * This class was copied and modified from + * https://github.com/OpenTracksApp/OSMDashboard/blob/main/src/main/java/de/storchp/opentracks/osmplugin/dashboardapi/Track.java + */ + private static final Logger LOG = LoggerFactory.getLogger(Track.class); + + private static final String TAG = Track.class.getSimpleName(); + + public static final String _ID = "_id"; + public static final String NAME = "name"; // track name + public static final String DESCRIPTION = "description"; // track description + public static final String CATEGORY = "category"; // track activity type + public static final String STARTTIME = "starttime"; // track start time + public static final String STOPTIME = "stoptime"; // track stop time + public static final String TOTALDISTANCE = "totaldistance"; // total distance + public static final String TOTALTIME = "totaltime"; // total time + public static final String MOVINGTIME = "movingtime"; // moving time + public static final String AVGSPEED = "avgspeed"; // average speed + public static final String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed + public static final String MAXSPEED = "maxspeed"; // maximum speed + public static final String MINELEVATION = "minelevation"; // minimum elevation + public static final String MAXELEVATION = "maxelevation"; // maximum elevation + public static final String ELEVATIONGAIN = "elevationgain"; // elevation gain + + public static final String[] PROJECTION = { + _ID, + NAME, + DESCRIPTION, + CATEGORY, + STARTTIME, + STOPTIME, + TOTALDISTANCE, + TOTALTIME, + MOVINGTIME, + AVGSPEED, + AVGMOVINGSPEED, + MAXSPEED, + MINELEVATION, + MAXELEVATION, + ELEVATIONGAIN + }; + + private final long id; + private final String trackname; + private final String description; + private final String category; + private final int startTimeEpochMillis; + private final int stopTimeEpochMillis; + private final float totalDistanceMeter; + private final int totalTimeMillis; + private final int movingTimeMillis; + private final float avgSpeedMeterPerSecond; + private final float avgMovingSpeedMeterPerSecond; + private final float maxSpeedMeterPerSecond; + private final float minElevationMeter; + private final float maxElevationMeter; + private final float elevationGainMeter; + + public Track(final long id, final String trackname, final String description, final String category, final int startTimeEpochMillis, final int stopTimeEpochMillis, final float totalDistanceMeter, final int totalTimeMillis, final int movingTimeMillis, final float avgSpeedMeterPerSecond, final float avgMovingSpeedMeterPerSecond, final float maxSpeedMeterPerSecond, final float minElevationMeter, final float maxElevationMeter, final float elevationGainMeter) { + this.id = id; + this.trackname = trackname; + this.description = description; + this.category = category; + this.startTimeEpochMillis = startTimeEpochMillis; + this.stopTimeEpochMillis = stopTimeEpochMillis; + this.totalDistanceMeter = totalDistanceMeter; + this.totalTimeMillis = totalTimeMillis; + this.movingTimeMillis = movingTimeMillis; + this.avgSpeedMeterPerSecond = avgSpeedMeterPerSecond; + this.avgMovingSpeedMeterPerSecond = avgMovingSpeedMeterPerSecond; + this.maxSpeedMeterPerSecond = maxSpeedMeterPerSecond; + this.minElevationMeter = minElevationMeter; + this.maxElevationMeter = maxElevationMeter; + this.elevationGainMeter = elevationGainMeter; + } + + /** + * Reads the Tracks from the Content Uri + */ + public static List readTracks(final ContentResolver resolver, final Uri data, final int protocolVersion) { + LOG.info("Loading track(s) from " + data); + + final ArrayList tracks = new ArrayList(); + try (final Cursor cursor = resolver.query(data, Track.PROJECTION, null, null, null)) { + while (cursor.moveToNext()) { + final long id = cursor.getLong(cursor.getColumnIndexOrThrow(Track._ID)); + final String trackname = cursor.getString(cursor.getColumnIndexOrThrow(Track.NAME)); + final String description = cursor.getString(cursor.getColumnIndexOrThrow(Track.DESCRIPTION)); + final String category = cursor.getString(cursor.getColumnIndexOrThrow(Track.CATEGORY)); + final int startTimeEpochMillis = cursor.getInt(cursor.getColumnIndexOrThrow(Track.STARTTIME)); + final int stopTimeEpochMillis = cursor.getInt(cursor.getColumnIndexOrThrow(Track.STOPTIME)); + final float totalDistanceMeter = cursor.getFloat(cursor.getColumnIndexOrThrow(Track.TOTALDISTANCE)); + final int totalTimeMillis = cursor.getInt(cursor.getColumnIndexOrThrow(Track.TOTALTIME)); + final int movingTimeMillis = cursor.getInt(cursor.getColumnIndexOrThrow(Track.MOVINGTIME)); + final float avgSpeedMeterPerSecond = cursor.getFloat(cursor.getColumnIndexOrThrow(Track.AVGSPEED)); + final float avgMovingSpeedMeterPerSecond = cursor.getFloat(cursor.getColumnIndexOrThrow(Track.AVGMOVINGSPEED)); + final float maxSpeedMeterPerSecond = cursor.getFloat(cursor.getColumnIndexOrThrow(Track.MAXSPEED)); + final float minElevationMeter = cursor.getFloat(cursor.getColumnIndexOrThrow(Track.MINELEVATION)); + final float maxElevationMeter = cursor.getFloat(cursor.getColumnIndexOrThrow(Track.MAXELEVATION)); + final float elevationGainMeter = cursor.getFloat(cursor.getColumnIndexOrThrow(Track.ELEVATIONGAIN)); + + LOG.info("New Track data received: distance=" + totalDistanceMeter + " time=" + totalTimeMillis); + + tracks.add(new Track(id, trackname, description, category, startTimeEpochMillis, stopTimeEpochMillis, + totalDistanceMeter, totalTimeMillis, movingTimeMillis, avgSpeedMeterPerSecond, avgMovingSpeedMeterPerSecond, maxSpeedMeterPerSecond, + minElevationMeter, maxElevationMeter, elevationGainMeter)); + } + } catch (final SecurityException e) { + LOG.warn("No permission to read track", e); + } catch (final Exception e) { + LOG.warn("Reading track failed", e); + } + return tracks; + } + + public float getElevationGainMeter() { + return elevationGainMeter; + } + + public float getMaxElevationMeter() { + return maxElevationMeter; + } + + public float getMinElevationMeter() { + return minElevationMeter; + } + + public float getMaxSpeedMeterPerSecond() { + return maxSpeedMeterPerSecond; + } + + public float getAvgMovingSpeedMeterPerSecond() { + return avgMovingSpeedMeterPerSecond; + } + + public float getAvgSpeedMeterPerSecond() { + return avgSpeedMeterPerSecond; + } + + public int getMovingTimeMillis() { + return movingTimeMillis; + } + + public int getTotalTimeMillis() { + return totalTimeMillis; + } + + public float getTotalDistanceMeter() { + return totalDistanceMeter; + } + + public int getStopTimeEpochMillis() { + return stopTimeEpochMillis; + } + + public int getStartTimeEpochMillis() { + return startTimeEpochMillis; + } + + public String getCategory() { + return category; + } + + public String getDescription() { + return description; + } + + public String getTrackname() { + return trackname; + } + + public long getId() { + return id; + } +} + +class TrackStatistics { + /** + * This class was copied and modified from + * https://github.com/OpenTracksApp/OSMDashboard/blob/main/src/main/java/de/storchp/opentracks/osmplugin/utils/TrackStatistics.java + */ + + private String category = "unknown"; + private int startTimeEpochMillis; + private int stopTimeEpochMillis; + private float totalDistanceMeter; + private int totalTimeMillis; + private int movingTimeMillis; + private float avgSpeedMeterPerSecond; + private float avgMovingSpeedMeterPerSecond; + private float maxSpeedMeterPerSecond; + private float minElevationMeter; + private float maxElevationMeter; + private float elevationGainMeter; + + public TrackStatistics(final List tracks) { + if (tracks.isEmpty()) { + return; + } + final Track first = tracks.get(0); + category = first.getCategory(); + startTimeEpochMillis = first.getStartTimeEpochMillis(); + stopTimeEpochMillis = first.getStopTimeEpochMillis(); + totalDistanceMeter = first.getTotalDistanceMeter(); + totalTimeMillis = first.getTotalTimeMillis(); + movingTimeMillis = first.getMovingTimeMillis(); + avgSpeedMeterPerSecond = first.getAvgSpeedMeterPerSecond(); + avgMovingSpeedMeterPerSecond = first.getAvgMovingSpeedMeterPerSecond(); + maxSpeedMeterPerSecond = first.getMaxSpeedMeterPerSecond(); + minElevationMeter = first.getMinElevationMeter(); + maxElevationMeter = first.getMaxElevationMeter(); + elevationGainMeter = first.getElevationGainMeter(); + + if (tracks.size() > 1) { + float totalAvgSpeedMeterPerSecond = avgSpeedMeterPerSecond; + float totalAvgMovingSpeedMeterPerSecond = avgMovingSpeedMeterPerSecond; + for (final Track track : tracks.subList(1, tracks.size())) { + if (!category.equals(track.getCategory())) { + category = "mixed"; + } + startTimeEpochMillis = Math.min(startTimeEpochMillis, track.getStartTimeEpochMillis()); + stopTimeEpochMillis = Math.max(stopTimeEpochMillis, track.getStopTimeEpochMillis()); + totalDistanceMeter += track.getTotalDistanceMeter(); + totalTimeMillis += track.getTotalTimeMillis(); + movingTimeMillis += track.getMovingTimeMillis(); + totalAvgSpeedMeterPerSecond += track.getAvgSpeedMeterPerSecond(); + totalAvgMovingSpeedMeterPerSecond += track.getAvgMovingSpeedMeterPerSecond(); + maxSpeedMeterPerSecond = Math.max(maxSpeedMeterPerSecond, track.getMaxSpeedMeterPerSecond()); + minElevationMeter = Math.min(minElevationMeter, track.getMinElevationMeter()); + maxElevationMeter = Math.max(maxElevationMeter, track.getMaxElevationMeter()); + elevationGainMeter += track.getElevationGainMeter(); + } + + avgSpeedMeterPerSecond = totalAvgSpeedMeterPerSecond / tracks.size(); + avgMovingSpeedMeterPerSecond = totalAvgMovingSpeedMeterPerSecond / tracks.size(); + } + } + + public String getCategory() { + return category; + } + + public int getStartTimeEpochMillis() { + return startTimeEpochMillis; + } + + public int getStopTimeEpochMillis() { + return stopTimeEpochMillis; + } + + public float getTotalDistanceMeter() { + return totalDistanceMeter; + } + + public int getTotalTimeMillis() { + return totalTimeMillis; + } + + public int getMovingTimeMillis() { + return movingTimeMillis; + } + + public float getAvgSpeedMeterPerSecond() { + return avgSpeedMeterPerSecond; + } + + public float getAvgMovingSpeedMeterPerSecond() { + return avgMovingSpeedMeterPerSecond; + } + + public float getMaxSpeedMeterPerSecond() { + return maxSpeedMeterPerSecond; + } + + public float getMinElevationMeter() { + return minElevationMeter; + } + + public float getMaxElevationMeter() { + return maxElevationMeter; + } + + public float getElevationGainMeter() { + return elevationGainMeter; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java new file mode 100644 index 000000000..3d94d8951 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java @@ -0,0 +1,116 @@ +/* Copyright (C) 2022 Arjan Schrijver + + 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 . */ + +package nodomain.freeyourgadget.gadgetbridge.externalevents; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class OpenTracksController extends Activity { + /* + * A short explanation of how this integration with OpenTracks works: + * Starting the recording from a device requires calling `startRecording()` + * on this class. For a simple example, check out the implementation in + * `WorkoutRequestHandler`, used by the Fossil HR series. + * The OpenTracks class can be set in the Gadgetbridge settings and depends + * on the installation source used for OpenTracks. Details can be found in + * their documentation here: https://github.com/OpenTracksApp/OpenTracks#api + * `startRecording()` sends an explicit Intent to OpenTracks signalling it + * to start recording. It passes along the package name and class name of + * our `OpenTracksController` which OpenTracks will use to send the + * statistics URIs to. After starting the recording service, OpenTracks + * uses a new explicit Intent to start our `OpenTracksController` and passes + * along the URIs and the read permissions for those URIs (using + * `Intent.FLAG_GRANT_READ_URI_PERMISSION`). So at that point + * `OpenTracksController` is started as a new `Activity` (or `Context`) + * which has the read permissions for the statistics URIs. The controller + * saves its `Context` into the `OpenTracksContentObserver` in the GB main + * process, so it can keep running and read the statistics with the correct + * `Context`. So, whatever class, device or activity calls the methods on + * the `OpenTracksContentObserver` from, it will always work. + */ + private static final String EXTRAS_PROTOCOL_VERSION = "PROTOCOL_VERSION"; + private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard"; + private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload"; + + private final Logger LOG = LoggerFactory.getLogger(OpenTracksController.class); + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + GBApplication gbApp = GBApplication.app(); + Intent intent = getIntent(); + int protocolVersion = intent.getIntExtra(EXTRAS_PROTOCOL_VERSION, 1); + final ArrayList uris = intent.getParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD); + if (uris != null) { + if (gbApp.getOpenTracksObserver() != null) { + LOG.info("Unregistering old OpenTracksContentObserver"); + gbApp.getOpenTracksObserver().unregister(); + } + Uri tracksUri = uris.get(0); + LOG.info("Registering OpenTracksContentObserver with tracks URI: " + tracksUri); + gbApp.setOpenTracksObserver(new OpenTracksContentObserver(this, tracksUri, protocolVersion)); + try { + getContentResolver().registerContentObserver(tracksUri, false, gbApp.getOpenTracksObserver()); + } catch (final SecurityException se) { + LOG.error("Error registering OpenTracksContentObserver", se); + } + } + moveTaskToBack(true); + } + + public static void sendIntent(Context context, String className) { + Prefs prefs = GBApplication.getPrefs(); + String packageName = prefs.getString("opentracks_packagename", "de.dennisguse.opentracks"); + Intent intent = new Intent(); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setClassName(packageName, className); + intent.putExtra("STATS_TARGET_PACKAGE", context.getPackageName()); + intent.putExtra("STATS_TARGET_CLASS", "nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController"); + try { + context.startActivity(intent); + } catch (Exception e) { + GB.toast(e.getMessage(), Toast.LENGTH_LONG, GB.WARN); + } + } + + public static void startRecording(Context context) { + sendIntent(context, "de.dennisguse.opentracks.publicapi.StartRecording"); + } + + public static void stopRecording(Context context) { + sendIntent(context, "de.dennisguse.opentracks.publicapi.StopRecording"); + OpenTracksContentObserver openTracksObserver = GBApplication.app().getOpenTracksObserver(); + if (openTracksObserver != null) { + openTracksObserver.finish(); + } + GBApplication.app().setOpenTracksObserver(null); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java index 542fa5fbc..e79089800 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java @@ -201,6 +201,9 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { batteryInfo.level = b; batteryInfo.state = BatteryState.BATTERY_NORMAL; } + if (json.has("chg") && json.getInt("chg") == 1) { + batteryInfo.state = BatteryState.BATTERY_CHARGING; + } if (json.has("volt")) batteryInfo.voltage = (float) json.getDouble("volt"); handleGBDeviceEvent(batteryInfo); @@ -407,9 +410,10 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { jsonalarms.put(jsonalarm); Calendar calendar = AlarmUtils.toCalendar(alarm); - // TODO: getRepetition to ensure it only happens on correct day? + jsonalarm.put("h", alarm.getHour()); jsonalarm.put("m", alarm.getMinute()); + jsonalarm.put("rep", alarm.getRepetition()); } uartTxJSON("onSetAlarms", o); } catch (JSONException e) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index ef15030a3..313ac3b1b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -93,6 +93,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; @@ -157,6 +158,8 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_BROADCAST; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_START; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_STOP; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_OFF; 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_FELL_SLEEP_SELECTION; @@ -1408,10 +1411,18 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { if (prefs.getBoolean(HuamiConst.PREF_BUTTON_ACTION_VIBRATE, false)) { vibrateOnce(); } - if (buttonPreference.equals(PREF_BUTTON_ACTION_SELECTION_BROADCAST)) { - sendSystemBroadcastWithButtonId(); - } else { - handleMediaButton(buttonPreference); + switch (buttonPreference) { + case PREF_BUTTON_ACTION_SELECTION_BROADCAST: + sendSystemBroadcastWithButtonId(); + break; + case PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_START: + OpenTracksController.startRecording(this.getContext()); + break; + case PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_STOP: + OpenTracksController.stopRecording(this.getContext()); + break; + default: + handleMediaButton(buttonPreference); } } @@ -1419,10 +1430,18 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { if (deviceAction.equals(PREF_DEVICE_ACTION_SELECTION_OFF)) { return; } - if (deviceAction.equals(PREF_DEVICE_ACTION_SELECTION_BROADCAST)) { - sendSystemBroadcast(message); - }else { - handleMediaButton(deviceAction); + switch (deviceAction) { + case PREF_BUTTON_ACTION_SELECTION_BROADCAST: + sendSystemBroadcast(message); + break; + case PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_START: + OpenTracksController.startRecording(this.getContext()); + break; + case PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_STOP: + OpenTracksController.stopRecording(this.getContext()); + break; + default: + handleMediaButton(deviceAction); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java index 376f84cd4..3f17f3029 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java @@ -144,6 +144,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomWidgetElement; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.Widget; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.WidgetsPutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.workout.WorkoutRequestHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.FactoryResetRequest; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -1558,13 +1559,25 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { return; } - queueWrite(new SetCommuteMenuMessage(getContext().getString(R.string.fossil_hr_commute_processing), false, this)); - Intent menuIntent = new Intent(QHybridSupport.QHYBRID_EVENT_COMMUTE_MENU); menuIntent.putExtra("EXTRA_ACTION", action); getContext().sendBroadcast(menuIntent); } else if (request.has("master._.config.app_status")) { queueWrite(new ConfirmAppStatusRequest(requestId, this)); + } else if (request.has("workoutApp")) { + JSONObject workoutRequest = request.getJSONObject("workoutApp"); + String workoutState = workoutRequest.optString("state"); + String workoutType = workoutRequest.optString("type"); + LOG.info("Got workoutApp request, state=" + workoutState + ", type=" + workoutType); + JSONObject workoutResponse = WorkoutRequestHandler.handleRequest(getContext(), requestId, workoutRequest); + if (workoutResponse.length() > 0) { + JSONObject responseObject = new JSONObject() + .put("res", new JSONObject() + .put("id", requestId) + .put("set", workoutResponse) + ); + queueWrite(new JsonPutRequest(responseObject, this)); + } } else { LOG.warn("Unhandled request from watch: " + requestJson.toString()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java new file mode 100644 index 000000000..fc69b4e39 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java @@ -0,0 +1,80 @@ +/* Copyright (C) 2022 Arjan Schrijver + + 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 . */ + +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.workout; + +import android.content.Context; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController; + +public class WorkoutRequestHandler { + public static void addStateResponse(JSONObject workoutResponse, String type, String msg) throws JSONException { + workoutResponse.put("workoutApp._.config.response", new JSONObject() + .put("message", msg) + .put("type", type) + ); + } + + public static JSONObject handleRequest(Context context, int requestId, JSONObject workoutRequest) throws JSONException { + final Logger LOG = LoggerFactory.getLogger(WorkoutRequestHandler.class); + + JSONObject workoutResponse = new JSONObject(); + if (workoutRequest.optString("state").equals("started") && workoutRequest.optString("gps").equals("on")) { + int activityType = workoutRequest.optInt("activity", -1); + LOG.info("Workout started, activity type is " + activityType); + addStateResponse(workoutResponse, "success", ""); + OpenTracksController.startRecording(context); + } else if (workoutRequest.optString("type").equals("req_distance")) { + long timeSecs = GBApplication.app().getOpenTracksObserver().getTimeMillisChange() / 1000; + float distanceCM = GBApplication.app().getOpenTracksObserver().getDistanceMeterChange() * 100; + LOG.info("Workout distance requested, returning " + distanceCM + " cm, " + timeSecs + " sec"); + workoutResponse.put("workoutApp._.config.gps", new JSONObject() + .put("distance", distanceCM) + .put("duration", timeSecs) + ); + } else if (workoutRequest.optString("state").equals("paused")) { + LOG.info("Workout paused"); + addStateResponse(workoutResponse, "success", ""); + // Pause OpenTracks recording? + } else if (workoutRequest.optString("state").equals("resumed")) { + LOG.info("Workout resumed"); + addStateResponse(workoutResponse, "success", ""); + // Resume OpenTracks recording? + } else if (workoutRequest.optString("state").equals("end")) { + LOG.info("Workout stopped"); + addStateResponse(workoutResponse, "success", ""); + OpenTracksController.stopRecording(context); + } else if (workoutRequest.optString("type").equals("req_route")) { + LOG.info("Workout route image requested, returning error"); + addStateResponse(workoutResponse, "error", ""); + // Send the traveled route as an RLE encoded image (example name: 58270405) + // Send back a JSON packet, example: + // {"res":{"id":21,"set":{"workoutApp._.config.images":{"session_id":1213693133,"route":{"name":"58270405"},"layout_type":"vertical"}}}} + // or + // {"res":{"id":34,"set":{"workoutApp._.config.images":{"session_id":504875,"route":{"name":"211631088"},"layout_type":"horizontal"}}}} + } else { + LOG.info("Request not recognized: " + workoutRequest); + } + return workoutResponse; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/um25/Support/UM25Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/um25/Support/UM25Support.java index 704dfd197..7a728c375 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/um25/Support/UM25Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/um25/Support/UM25Support.java @@ -2,7 +2,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -29,10 +32,12 @@ public class UM25Support extends UM25BaseSupport { public static final String UUID_CHAR = "0000ffe1-0000-1000-8000-00805f9b34fb"; public static final String ACTION_MEASUREMENT_TAKEN = "com.nodomain.gadgetbridge.um25.MEASUREMENT_TAKEN"; + public static final String ACTION_RESET_STATS = "com.nodomain.gadgetbridge.um25.RESET_STATS"; public static final String EXTRA_KEY_MEASUREMENT_DATA = "EXTRA_MEASUREMENT_DATA"; public static final int LOOP_DELAY = 500; private final byte[] COMMAND_UPDATE = new byte[]{(byte) 0xF0}; + private final byte[] COMMAND_RESET_STATS = new byte[]{(byte) 0xF4}; private final int PAYLOAD_LENGTH = 130; private ByteBuffer buffer = ByteBuffer.allocate(PAYLOAD_LENGTH); @@ -46,6 +51,18 @@ public class UM25Support extends UM25BaseSupport { this.buffer.mark(); } + BroadcastReceiver resetReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if(!ACTION_RESET_STATS.equals(intent.getAction())){ + return; + } + new TransactionBuilder("reset stats") + .write(getCharacteristic(UUID.fromString(UUID_CHAR)), COMMAND_RESET_STATS) + .queue(getQueue()); + } + }; + @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { return builder @@ -60,6 +77,11 @@ public class UM25Support extends UM25BaseSupport { @Override public boolean run(BluetoothGatt gatt) { logger.debug("initialized, starting timers"); + LocalBroadcastManager.getInstance(getContext()) + .registerReceiver( + resetReceiver, + new IntentFilter(ACTION_RESET_STATS) + ); startLoop(); return true; } @@ -67,6 +89,13 @@ public class UM25Support extends UM25BaseSupport { .add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); } + @Override + public void dispose() { + super.dispose(); + LocalBroadcastManager.getInstance(getContext()) + .unregisterReceiver(resetReceiver); + } + private void startLoop(){ ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); executor.scheduleWithFixedDelay(new Runnable() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java index b45739fd7..15406e035 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java @@ -194,7 +194,15 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport { @Override public void onNotification(NotificationSpec notificationSpec) { - //TODO: Implement + try { + TransactionBuilder builder = performInitialized("xwatch notification"); + BluetoothGattCharacteristic deviceData = getCharacteristic(XWatchService.UUID_WRITE); + byte[] data = new byte[]{XWatchService.COMMAND_NOTIFICATION, XWatchService.COMMAND_NOTIFICATION_MESSAGE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + builder.write(deviceData, crcChecksum(data)); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send message notification on XWatch device", ex); + } } @Override @@ -215,7 +223,18 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport { @Override public void onSetCallState(CallSpec callSpec) { - //TODO: Implement (if necessary) + if (callSpec.command == CallSpec.CALL_INCOMING) { + LOG.debug("Incoming call8"); + try { + TransactionBuilder builder = performInitialized("callnotification"); + BluetoothGattCharacteristic deviceData = getCharacteristic(XWatchService.UUID_WRITE); + byte[] data = new byte[]{XWatchService.COMMAND_NOTIFICATION, XWatchService.COMMAND_NOTIFICATION_PHONE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + builder.write(deviceData, crcChecksum(data)); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send call notification on XWatch device", ex); + } + } } @Override @@ -260,7 +279,7 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport { @Override public void onFetchRecordedData(int dataTypes) { try { - if(builder == null) { + if (builder == null) { builder = performInitialized("fetchActivityData"); } requestSummarizedData(builder); @@ -456,7 +475,7 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport { if (value[5] == 95) { dayToFetch++; - if(dayToFetch <= maxDayToFetch) { + if (dayToFetch <= maxDayToFetch) { try { builder = performInitialized("fetchActivityData"); requestDetailedData(builder); @@ -475,8 +494,8 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport { private void handleButtonPressed(byte[] value) { long currentTimestamp = System.currentTimeMillis(); - AudioManager audioManager = (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE); - if(audioManager.isWiredHeadsetOn()) { + AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); + if (audioManager.isWiredHeadsetOn()) { if (currentTimestamp - lastButtonTimestamp < 1000) { if (audioManager.isMusicActive()) { audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); @@ -570,7 +589,7 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport { minutes ); - timestamp = (int)(cal.getTimeInMillis() / 1000); + timestamp = (int) (cal.getTimeInMillis() / 1000); return timestamp; } diff --git a/app/src/main/res/layout/activity_data_management.xml b/app/src/main/res/layout/activity_data_management.xml index d72cb5216..0c2489d00 100644 --- a/app/src/main/res/layout/activity_data_management.xml +++ b/app/src/main/res/layout/activity_data_management.xml @@ -100,7 +100,7 @@ android:text="@string/activity_db_management_clean_export_directory_label" /> + + + + + + + +