Merge branch 'master' of codeberg.org:Freeyourgadget/Gadgetbridge into multi-device-support

This commit is contained in:
Daniel Dakhno 2022-02-22 01:56:14 +01:00
commit 68a7f22d47
17 changed files with 673 additions and 14 deletions

View File

@ -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.*

View File

@ -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.*

View File

@ -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:

View File

@ -668,5 +668,10 @@
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<activity
android:name=".externalevents.OpenTracksController"
android:label="OpenTracks controller and intent receiver"
android:exported="true"/>
</application>
</manifest>

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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

View File

@ -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")
) {

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<Track> 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<Track> readTracks(final ContentResolver resolver, final Uri data, final int protocolVersion) {
LOG.info("Loading track(s) from " + data);
final ArrayList<Track> tracks = new ArrayList<Track>();
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<Track> 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;
}
}

View File

@ -0,0 +1,93 @@
/* 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 <http://www.gnu.org/licenses/>. */
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 {
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<Uri> 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();
}
}
}

View File

@ -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);
}
}

View File

@ -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;
@ -1565,6 +1566,20 @@ 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 = 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());
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -1654,6 +1654,12 @@
<item>@string/pref_media_forward</item>
<item>@string/pref_media_rewind</item>
<item>@string/pref_device_action_broadcast</item>
<item>@string/pref_device_action_fitness_app_control_start</item>
<!--
Enable when OpenTracks allows to be stopped via intent
https://github.com/OpenTracksApp/OpenTracks/issues/1103
<item>@string/pref_device_action_fitness_app_control_stop</item>
-->
</string-array>
<string-array name="button_action_values">
@ -1668,6 +1674,12 @@
<item>@string/pref_media_forward_value</item>
<item>@string/pref_media_rewind_value</item>
<item>@string/pref_device_action_broadcast_value</item>
<item>@string/pref_device_action_fitness_app_control_start_value</item>
<!--
Enable when OpenTracks allows to be stopped via intent
https://github.com/OpenTracksApp/OpenTracks/issues/1103
<item>@string/pref_device_action_fitness_app_control_stop_value</item>
-->
</string-array>
<string-array name="device_action_options">
@ -1676,6 +1688,12 @@
<item>@string/pref_media_pause</item>
<item>@string/pref_media_playpause</item>
<item>@string/pref_device_action_broadcast</item>
<item>@string/pref_device_action_fitness_app_control_start</item>
<!--
Enable when OpenTracks allows to be stopped via intent
https://github.com/OpenTracksApp/OpenTracks/issues/1103
<item>@string/pref_device_action_fitness_app_control_stop</item>
-->
</string-array>
<string-array name="device_action_values">
@ -1684,6 +1702,12 @@
<item>@string/pref_media_pause_value</item>
<item>@string/pref_media_playpause_value</item>
<item>@string/pref_device_action_broadcast_value</item>
<item>@string/pref_device_action_fitness_app_control_start_value</item>
<!--
Enable when OpenTracks allows to be stopped via intent
https://github.com/OpenTracksApp/OpenTracks/issues/1103
<item>@string/pref_device_action_fitness_app_control_stop_value</item>
-->
</string-array>
<string-array name="pref_hybridhr_buttonfunctions">

View File

@ -1495,6 +1495,8 @@
<string name="pref_media_forward">Skip forward</string>
<string name="pref_media_rewind">Skip back</string>
<string name="pref_device_action_broadcast">Send Broadcast</string>
<string name="pref_device_action_fitness_app_control_start">Fitness App Tracking Start</string>
<string name="pref_device_action_fitness_app_control_stop">Fitness App Tracking Stop</string>
<!-- Translators: the ### indicate number of digits, keep intact -->
<string name="distance_format_meters">###m</string>
<!-- Translators: the ### indicate number of digits, keep intact -->
@ -1516,6 +1518,8 @@
<string name="menuitem_menu">Menu</string>
<string name="fossil_hr_button_config_info">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.</string>
<string name="watchface_dialog_widget_width">Width:</string>
<string name="pref_title_opentracks_packagename">OpenTracks package name</string>
<string name="pref_summary_opentracks_packagename">Used for starting/stopping GPS track recording from certain wearables that don\'t have built-in GPS</string>
<string name="info_no_devices_connected">no devices connected</string>
<string name="info_connected_count">%d devices connected</string>

View File

@ -106,6 +106,8 @@
<item name="pref_media_forward_value" translatable="false" type="string">FORWARD</item>
<item name="pref_media_rewind_value" translatable="false" type="string">REWIND</item>
<item name="pref_device_action_broadcast_value" translatable="false" type="string">BROADCAST</item>
<item name="pref_device_action_fitness_app_control_start_value" translatable="false" type="string">FITNESS_CONTROL_START</item>
<item name="pref_device_action_fitness_app_control_stop_value" translatable="false" type="string">FITNESS_CONTROL_STOP</item>
<item name="pref_title_touch_voice_assistant" type="string">Voice Assistant</item>
<item name="pref_title_touch_anc" type="string">Active Noise Cancelling</item>
<item name="pref_title_touch_quick_ambient" type="string">Quick Ambient Sound</item>

View File

@ -143,6 +143,12 @@
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_summary_location_keep_uptodate"
android:title="@string/pref_title_location_keep_uptodate" />
<EditTextPreference
android:inputType="text"
android:key="opentracks_packagename"
android:defaultValue="de.dennisguse.opentracks"
android:title="@string/pref_title_opentracks_packagename"
android:summary="@string/pref_summary_opentracks_packagename" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences_category_device_specific_settings">