From 93fc2c3b617f55d857984abbec0da598dbc66db8 Mon Sep 17 00:00:00 2001 From: Daniel Dakhno Date: Mon, 14 Feb 2022 19:12:44 +0100 Subject: [PATCH 001/108] VESC: fixed crash on loading saved integer --- .../gadgetbridge/devices/vesc/VescControlActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From c8ad21eebf2b5a66e6248dfcb789d0de3a929108 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Sun, 30 Jan 2022 22:10:29 +0100 Subject: [PATCH 002/108] Fossil Hybrid HR: Start/stop track in OpenTracks from GPS workout on watch --- .../fossil_hr/FossilHRWatchAdapter.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) 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..d836143bc 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 @@ -1565,6 +1565,72 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { 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 = 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 " + Integer.toString(activityType)); + workoutResponse.put("workoutApp._.config.response", new JSONObject() + .put("message", "") + .put("type", "success") + ); + Intent intent = new Intent(); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setClassName("de.dennisguse.opentracks.debug", "de.dennisguse.opentracks.publicapi.StartRecording"); + getContext().startActivity(intent); + } + if (workoutRequest.optString("type").equals("req_distance")) { + workoutResponse.put("workoutApp._.config.gps", new JSONObject() + .put("distance", -2) + .put("duration", 10) + ); + } + if (workoutRequest.optString("state").equals("paused")) { + LOG.info("Workout paused"); + workoutResponse.put("workoutApp._.config.response", new JSONObject() + .put("message", "") + .put("type", "success") + ); + // Pause OpenTracks recording? + } + if (workoutRequest.optString("state").equals("resumed")) { + LOG.info("Workout resumed"); + workoutResponse.put("workoutApp._.config.response", new JSONObject() + .put("message", "") + .put("type", "success") + ); + // Resume OpenTracks recording? + } + if (workoutRequest.optString("state").equals("end")) { + LOG.info("Workout stopped"); + workoutResponse.put("workoutApp._.config.response", new JSONObject() + .put("message", "") + .put("type", "success") + ); + Intent intent = new Intent(); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setClassName("de.dennisguse.opentracks.debug", "de.dennisguse.opentracks.publicapi.StopRecording"); + getContext().startActivity(intent); + } + if (workoutRequest.optString("type").equals("req_route")) { + // 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"}}}} + } + 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()); } From e188e54622702a798d7da1aa7a1b77464c1c9848 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Sun, 6 Feb 2022 21:36:25 +0100 Subject: [PATCH 003/108] Add OpenTracksController for interactions with OpenTracks --- .../externalevents/OpenTracksController.java | 56 +++++++++++++++++++ .../fossil_hr/FossilHRWatchAdapter.java | 11 +--- app/src/main/res/xml/preferences.xml | 6 ++ 3 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java 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..a94ba03d0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java @@ -0,0 +1,56 @@ +/* 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class OpenTracksController extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + Bundle bundle = intent.getExtras(); + if (bundle != null) { + // Handle received OpenTracks Dashboard API intent + } + } + } + + 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); + context.startActivity(intent); + } + + 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"); + } +} \ No newline at end of file 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 d836143bc..b1c60ee1d 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 @@ -86,6 +86,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySamp import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration; import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample; import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener; +import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -1578,10 +1579,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { .put("message", "") .put("type", "success") ); - Intent intent = new Intent(); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setClassName("de.dennisguse.opentracks.debug", "de.dennisguse.opentracks.publicapi.StartRecording"); - getContext().startActivity(intent); + OpenTracksController.startRecording(getContext()); } if (workoutRequest.optString("type").equals("req_distance")) { workoutResponse.put("workoutApp._.config.gps", new JSONObject() @@ -1611,10 +1609,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { .put("message", "") .put("type", "success") ); - Intent intent = new Intent(); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setClassName("de.dennisguse.opentracks.debug", "de.dennisguse.opentracks.publicapi.StopRecording"); - getContext().startActivity(intent); + OpenTracksController.stopRecording(getContext()); } if (workoutRequest.optString("type").equals("req_route")) { // Send the traveled route as an RLE encoded image (example name: 58270405) diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6b930337c..b7fcb73a3 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -141,6 +141,12 @@ android:layout="@layout/preference_checkbox" android:summary="@string/pref_summary_location_keep_uptodate" android:title="@string/pref_title_location_keep_uptodate" /> + From d2408f77cd6bd1a7be1a5c15d84117c24b3e905c Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Sun, 6 Feb 2022 22:18:41 +0100 Subject: [PATCH 004/108] Fossil Hybrid HR: Move workout request handling to separate class --- .../fossil_hr/FossilHRWatchAdapter.java | 50 +----------- .../workout/WorkoutRequestHandler.java | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java 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 b1c60ee1d..7e2e57498 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 @@ -86,7 +86,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySamp import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration; import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample; import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener; -import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -145,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; @@ -1571,53 +1571,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { String workoutState = workoutRequest.optString("state"); String workoutType = workoutRequest.optString("type"); LOG.info("Got workoutApp request, state=" + workoutState + ", type=" + workoutType); - 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 " + Integer.toString(activityType)); - workoutResponse.put("workoutApp._.config.response", new JSONObject() - .put("message", "") - .put("type", "success") - ); - OpenTracksController.startRecording(getContext()); - } - if (workoutRequest.optString("type").equals("req_distance")) { - workoutResponse.put("workoutApp._.config.gps", new JSONObject() - .put("distance", -2) - .put("duration", 10) - ); - } - if (workoutRequest.optString("state").equals("paused")) { - LOG.info("Workout paused"); - workoutResponse.put("workoutApp._.config.response", new JSONObject() - .put("message", "") - .put("type", "success") - ); - // Pause OpenTracks recording? - } - if (workoutRequest.optString("state").equals("resumed")) { - LOG.info("Workout resumed"); - workoutResponse.put("workoutApp._.config.response", new JSONObject() - .put("message", "") - .put("type", "success") - ); - // Resume OpenTracks recording? - } - if (workoutRequest.optString("state").equals("end")) { - LOG.info("Workout stopped"); - workoutResponse.put("workoutApp._.config.response", new JSONObject() - .put("message", "") - .put("type", "success") - ); - OpenTracksController.stopRecording(getContext()); - } - if (workoutRequest.optString("type").equals("req_route")) { - // 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"}}}} - } + JSONObject workoutResponse = WorkoutRequestHandler.handleRequest(getContext(), requestId, workoutRequest); if (workoutResponse.length() > 0) { JSONObject responseObject = new JSONObject() .put("res", new JSONObject() 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..23d502e66 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java @@ -0,0 +1,77 @@ +/* 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.externalevents.OpenTracksController; + +public class WorkoutRequestHandler { + public static void addStateResponse(JSONObject workoutResponse, String type) throws JSONException { + workoutResponse.put("workoutApp._.config.response", new JSONObject() + .put("message", "") + .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); + } + if (workoutRequest.optString("type").equals("req_distance")) { + workoutResponse.put("workoutApp._.config.gps", new JSONObject() + .put("distance", -2) + .put("duration", 10) + ); + } + if (workoutRequest.optString("state").equals("paused")) { + LOG.info("Workout paused"); + addStateResponse(workoutResponse, "success"); + // Pause OpenTracks recording? + } + if (workoutRequest.optString("state").equals("resumed")) { + LOG.info("Workout resumed"); + addStateResponse(workoutResponse, "success"); + // Resume OpenTracks recording? + } + if (workoutRequest.optString("state").equals("end")) { + LOG.info("Workout stopped"); + addStateResponse(workoutResponse, "success"); + OpenTracksController.stopRecording(context); + } + if (workoutRequest.optString("type").equals("req_route")) { + // 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"}}}} + } + return workoutResponse; + } +} \ No newline at end of file From 5224304b6ce51e88c7ca9f1fa968d6df56893fac Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 8 Feb 2022 13:38:23 +0100 Subject: [PATCH 005/108] Read overall statistics from received OpenTracks dashboard API URIs --- app/src/main/AndroidManifest.xml | 5 + .../gadgetbridge/GBApplication.java | 11 + .../OpenTracksContentObserver.java | 393 ++++++++++++++++++ .../externalevents/OpenTracksController.java | 42 +- .../workout/WorkoutRequestHandler.java | 39 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 4 +- 7 files changed, 469 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksContentObserver.java 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/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 1b95e6d4d..d035cb2e2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -67,6 +67,7 @@ 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 +144,8 @@ public class GBApplication extends Application { private DeviceManager deviceManager; private BluetoothStateChangeReceiver bluetoothStateChangeReceiver; + private OpenTracksContentObserver openTracksObserver; + public static void quit() { GB.log("Quitting Gadgetbridge...", GB.INFO, null); Intent quitIntent = new Intent(GBApplication.ACTION_QUIT); @@ -1090,4 +1093,12 @@ public class GBApplication extends Application { return "Gadgetbridge"; } } + + public void setOpenTracksObserver(OpenTracksContentObserver openTracksObserver) { + this.openTracksObserver = openTracksObserver; + } + + public OpenTracksContentObserver getOpenTracksObserver() { + return openTracksObserver; + } } 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 index a94ba03d0..4a9f5de99 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java @@ -17,24 +17,49 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents; -import android.content.BroadcastReceiver; +import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class OpenTracksController extends BroadcastReceiver { +public class OpenTracksController extends Activity { + 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 onReceive(Context context, Intent intent) { - if (intent != null) { - Bundle bundle = intent.getExtras(); - if (bundle != null) { - // Handle received OpenTracks Dashboard API intent + 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) { @@ -43,6 +68,8 @@ public class OpenTracksController extends BroadcastReceiver { 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"); context.startActivity(intent); } @@ -52,5 +79,6 @@ public class OpenTracksController extends BroadcastReceiver { public static void stopRecording(Context context) { sendIntent(context, "de.dennisguse.opentracks.publicapi.StopRecording"); + GBApplication.app().getOpenTracksObserver().finish(); } } \ No newline at end of file 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 index 23d502e66..fc69b4e39 100644 --- 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 @@ -24,12 +24,13 @@ 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) throws JSONException { + public static void addStateResponse(JSONObject workoutResponse, String type, String msg) throws JSONException { workoutResponse.put("workoutApp._.config.response", new JSONObject() - .put("message", "") + .put("message", msg) .put("type", type) ); } @@ -41,36 +42,38 @@ public class WorkoutRequestHandler { 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"); + addStateResponse(workoutResponse, "success", ""); OpenTracksController.startRecording(context); - } - if (workoutRequest.optString("type").equals("req_distance")) { + } 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", -2) - .put("duration", 10) + .put("distance", distanceCM) + .put("duration", timeSecs) ); - } - if (workoutRequest.optString("state").equals("paused")) { + } else if (workoutRequest.optString("state").equals("paused")) { LOG.info("Workout paused"); - addStateResponse(workoutResponse, "success"); + addStateResponse(workoutResponse, "success", ""); // Pause OpenTracks recording? - } - if (workoutRequest.optString("state").equals("resumed")) { + } else if (workoutRequest.optString("state").equals("resumed")) { LOG.info("Workout resumed"); - addStateResponse(workoutResponse, "success"); + addStateResponse(workoutResponse, "success", ""); // Resume OpenTracks recording? - } - if (workoutRequest.optString("state").equals("end")) { + } else if (workoutRequest.optString("state").equals("end")) { LOG.info("Workout stopped"); - addStateResponse(workoutResponse, "success"); + addStateResponse(workoutResponse, "success", ""); OpenTracksController.stopRecording(context); - } - if (workoutRequest.optString("type").equals("req_route")) { + } 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; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d68dc91c..de7113abd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1516,4 +1516,6 @@ Menu Some buttons cannot be configured because their functions are hard-coded in the watch firmware.\n\nWarning: long-pressing the upper button when a watchface from the official Fossil app is installed will also toggle between showing/hiding widgets. Width: + OpenTracks package name + Used for starting/stopping GPS track recording from certain wearables that don\'t have built-in GPS diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b7fcb73a3..4d8bcbd10 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -145,8 +145,8 @@ android:inputType="text" android:key="opentracks_packagename" android:defaultValue="de.dennisguse.opentracks" - android:title="OpenTracks package name" - android:summary="Used for starting/stopping GPS track recording from certain wearables that don't have built-in GPS" /> + android:title="@string/pref_title_opentracks_packagename" + android:summary="@string/pref_summary_opentracks_packagename" /> From 65cbea6713a29da4b497c152059390895a43cb55 Mon Sep 17 00:00:00 2001 From: vanous Date: Sat, 19 Feb 2022 16:04:48 +0100 Subject: [PATCH 006/108] Add HUAMI button/device action to control fitness tracking - Fitness App Tracking Start already works, Stop is commented out for time being - Catch exception in case the package name for controlled app doesn't exist/work --- .../devices/huami/HuamiConst.java | 2 ++ .../externalevents/OpenTracksController.java | 8 ++++- .../service/devices/huami/HuamiSupport.java | 35 ++++++++++++++----- app/src/main/res/values/arrays.xml | 24 +++++++++++++ app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/values/values.xml | 2 ++ 6 files changed, 64 insertions(+), 9 deletions(-) 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/externalevents/OpenTracksController.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java index 4a9f5de99..3ebefaf92 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java @@ -22,6 +22,7 @@ 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; @@ -29,6 +30,7 @@ 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 { @@ -70,7 +72,11 @@ public class OpenTracksController extends Activity { intent.setClassName(packageName, className); intent.putExtra("STATS_TARGET_PACKAGE", context.getPackageName()); intent.putExtra("STATS_TARGET_CLASS", "nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController"); - context.startActivity(intent); + try { + context.startActivity(intent); + } catch (Exception e) { + GB.toast(e.getMessage(), Toast.LENGTH_LONG, GB.WARN); + } } public static void startRecording(Context context) { 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/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 3438d69ad..635c391fe 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1654,6 +1654,12 @@ @string/pref_media_forward @string/pref_media_rewind @string/pref_device_action_broadcast + @string/pref_device_action_fitness_app_control_start + @@ -1668,6 +1674,12 @@ @string/pref_media_forward_value @string/pref_media_rewind_value @string/pref_device_action_broadcast_value + @string/pref_device_action_fitness_app_control_start_value + @@ -1676,6 +1688,12 @@ @string/pref_media_pause @string/pref_media_playpause @string/pref_device_action_broadcast + @string/pref_device_action_fitness_app_control_start + @@ -1684,6 +1702,12 @@ @string/pref_media_pause_value @string/pref_media_playpause_value @string/pref_device_action_broadcast_value + @string/pref_device_action_fitness_app_control_start_value + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de7113abd..7189f706c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1495,6 +1495,8 @@ Skip forward Skip back Send Broadcast + Fitness App Tracking Start + Fitness App Tracking Stop ###m diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index d606a27a0..d8343afb7 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -106,6 +106,8 @@ FORWARD REWIND BROADCAST + FITNESS_CONTROL_START + FITNESS_CONTROL_STOP Voice Assistant Active Noise Cancelling Quick Ambient Sound From 020c4aacd5c998cdcb385ff505122a7d8a3bb23e Mon Sep 17 00:00:00 2001 From: vanous Date: Sun, 20 Feb 2022 15:07:50 +0100 Subject: [PATCH 007/108] add OpenTracks Nightly into whitelisted persistent notification apps --- .../gadgetbridge/externalevents/NotificationListener.java | 1 + 1 file changed, 1 insertion(+) 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") ) { From fdfb9f57e2fab647bfabe6ab27eab1d285a35e75 Mon Sep 17 00:00:00 2001 From: vanous Date: Sun, 20 Feb 2022 15:09:05 +0100 Subject: [PATCH 008/108] perform check before closing openTracksObserver.finish --- .../gadgetbridge/externalevents/OpenTracksController.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java index 3ebefaf92..7740bbe4e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java @@ -85,6 +85,9 @@ public class OpenTracksController extends Activity { public static void stopRecording(Context context) { sendIntent(context, "de.dennisguse.opentracks.publicapi.StopRecording"); - GBApplication.app().getOpenTracksObserver().finish(); + OpenTracksContentObserver openTracksObserver = GBApplication.app().getOpenTracksObserver(); + if (openTracksObserver != null) { + openTracksObserver.finish(); + } } } \ No newline at end of file From b5632e91d0b091bbe24978f5eaddc8e8cecdcbf1 Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Feb 2022 21:31:00 +0100 Subject: [PATCH 009/108] Update issue templates: indicate if user upgraded from Play Store version, more details for Device request --- .github/ISSUE_TEMPLATE.md | 3 +-- .github/ISSUE_TEMPLATE/bug_report.md | 3 +-- .github/ISSUE_TEMPLATE/device_request.md | 6 ++++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index cf4686c60..2d3ec1e6f 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.* diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cf4686c60..2d3ec1e6f 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.* 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: From 79e0c01f60ae51adef83e31db8d4d990ede7a922 Mon Sep 17 00:00:00 2001 From: vanous Date: Sun, 20 Feb 2022 15:43:52 +0100 Subject: [PATCH 010/108] add debug features for openTracksObserver --- .../activities/DebugActivity.java | 71 +++++++++++++++++++ app/src/main/res/layout/activity_debug.xml | 23 ++++++ 2 files changed, 94 insertions(+) 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/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml index 0530ac90c..19de06c28 100644 --- a/app/src/main/res/layout/activity_debug.xml +++ b/app/src/main/res/layout/activity_debug.xml @@ -233,6 +233,29 @@ grid:layout_columnSpan="2" grid:layout_gravity="fill_horizontal" /> +