diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java index ab52d60fe..0dc89863f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java @@ -333,8 +333,10 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { // // Calendar // - settings.add(R.xml.devicesettings_header_calendar); - settings.add(R.xml.devicesettings_sync_calendar); + if (supportsCalendarEvents()) { + settings.add(R.xml.devicesettings_header_calendar); + settings.add(R.xml.devicesettings_sync_calendar); + } // // Other diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index 360dbcce9..8c5e45785 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -274,8 +274,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { @Override public void onSetGpsLocation(final Location location) { - // TODO onSetGpsLocation - super.onSetGpsLocation(location); + healthService.onSetGpsLocation(location); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java index 8bc63b325..35cb4ecca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java @@ -17,6 +17,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services; import android.content.Intent; +import android.location.Location; +import android.os.Build; +import android.os.Handler; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -27,11 +30,9 @@ import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; -import java.util.List; import java.util.Locale; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -42,9 +43,10 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePref import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; -import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample; +import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager; +import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -77,6 +79,9 @@ public class XiaomiHealthService extends AbstractXiaomiService { private static final int CMD_CONFIG_STANDING_REMINDER_SET = 13; private static final int CMD_CONFIG_STRESS_GET = 14; private static final int CMD_CONFIG_STRESS_SET = 15; + private static final int CMD_WORKOUT_WATCH_STATUS = 26; + private static final int CMD_WORKOUT_WATCH_OPEN = 30; + private static final int CMD_WORKOUT_LOCATION = 48; private static final int CMD_REALTIME_STATS_START = 45; private static final int CMD_REALTIME_STATS_STOP = 46; private static final int CMD_REALTIME_STATS_EVENT = 47; @@ -84,10 +89,20 @@ public class XiaomiHealthService extends AbstractXiaomiService { private static final int GENDER_MALE = 1; private static final int GENDER_FEMALE = 2; + private static final int WORKOUT_STARTED = 0; + private static final int WORKOUT_RESUMED = 1; + private static final int WORKOUT_PAUSED = 2; + private static final int WORKOUT_FINISHED = 3; + private boolean realtimeStarted = false; private boolean realtimeOneShot = false; private int previousSteps = -1; + private boolean gpsStarted = false; + private boolean gpsFixAcquired = false; + private boolean workoutStarted = false; + private final Handler gpsTimeoutHandler = new Handler(); + private final XiaomiActivityFileFetcher activityFetcher = new XiaomiActivityFileFetcher(this); public XiaomiHealthService(final XiaomiSupport support) { @@ -116,6 +131,12 @@ public class XiaomiHealthService extends AbstractXiaomiService { case CMD_CONFIG_STRESS_GET: handleStressConfig(cmd.getHealth().getStress()); return; + case CMD_WORKOUT_WATCH_STATUS: + handleWorkoutStatus(cmd.getHealth().getWorkoutStatusWatch()); + return; + case CMD_WORKOUT_WATCH_OPEN: + handleWorkoutOpen(cmd.getHealth().getWorkoutOpenWatch()); + return; case CMD_REALTIME_STATS_EVENT: handleRealtimeStats(cmd.getHealth().getRealTimeStats()); return; @@ -406,6 +427,128 @@ public class XiaomiHealthService extends AbstractXiaomiService { ); } + private void handleWorkoutOpen(final XiaomiProto.WorkoutOpenWatch workoutOpenWatch) { + LOG.debug("Workout open on watch: {}", workoutOpenWatch.getSport()); + + workoutStarted = false; + + final boolean sendGpsToBand = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false); + if (!sendGpsToBand) { + getSupport().sendCommand( + "send location disabled", + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_WORKOUT_WATCH_OPEN) + .setHealth(XiaomiProto.Health.newBuilder().setWorkoutOpenReply( + XiaomiProto.WorkoutOpenReply.newBuilder() + .setUnknown1(3) + .setUnknown2(2) + .setUnknown3(10) + )) + .build() + ); + return; + } + + if (!gpsStarted) { + gpsStarted = true; + gpsFixAcquired = false; + GBLocationManager.start(getSupport().getContext(), getSupport()); + } + + gpsTimeoutHandler.removeCallbacksAndMessages(null); + // Timeout if the watch stops sending workout open + gpsTimeoutHandler.postDelayed(() -> GBLocationManager.stop(getSupport().getContext(), getSupport()), 5000); + } + + private void handleWorkoutStatus(final XiaomiProto.WorkoutStatusWatch workoutStatus) { + LOG.debug("Got workout status: {}", workoutStatus.getStatus()); + + final boolean startOnPhone = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_START_ON_PHONE, false); + + switch (workoutStatus.getStatus()) { + case WORKOUT_STARTED: + workoutStarted = true; + gpsTimeoutHandler.removeCallbacksAndMessages(null); + if (startOnPhone) { + OpenTracksController.startRecording(getSupport().getContext(), sportToActivityKind(workoutStatus.getSport())); + } + break; + case WORKOUT_RESUMED: + case WORKOUT_PAUSED: + break; + case WORKOUT_FINISHED: + GBLocationManager.stop(getSupport().getContext(), getSupport()); + if (startOnPhone) { + OpenTracksController.stopRecording(getSupport().getContext()); + } + break; + } + } + + public void onSetGpsLocation(final Location location) { + if (!gpsFixAcquired) { + gpsFixAcquired = true; + getSupport().sendCommand( + "send gps fix", + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_WORKOUT_WATCH_OPEN) + .setHealth(XiaomiProto.Health.newBuilder().setWorkoutOpenReply( + XiaomiProto.WorkoutOpenReply.newBuilder() + .setUnknown1(0) + .setUnknown2(2) + .setUnknown3(10) + )) + .build() + ); + } + + if (workoutStarted) { + final XiaomiProto.WorkoutLocation.Builder workoutLocation = XiaomiProto.WorkoutLocation.newBuilder() + .setNumSatellites(10) + .setTimestamp((int) (location.getTime() / 1000L)) + .setLongitude(location.getLongitude()) + .setLatitude(location.getLatitude()) + .setAltitude(location.getAltitude()) + .setSpeed(location.getSpeed()) + .setBearing(location.getBearing()) + .setHorizontalAccuracy(location.getAccuracy()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + workoutLocation.setVerticalAccuracy(location.getVerticalAccuracyMeters()); + } + + getSupport().sendCommand( + "send gps location", + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_WORKOUT_LOCATION) + .setHealth(XiaomiProto.Health.newBuilder().setWorkoutLocation(workoutLocation)) + .build() + ); + } + } + + private int sportToActivityKind(final int sport) { + switch (sport) { + case 1: // outdoor run + case 5: // trail run + return ActivityKind.TYPE_RUNNING; + case 2: + return ActivityKind.TYPE_WALKING; + case 3: // hiking + case 4: // trekking + return ActivityKind.TYPE_HIKING; + case 6: + return ActivityKind.TYPE_CYCLING; + } + + LOG.warn("Unknown sport {}", sport); + + return ActivityKind.TYPE_UNKNOWN; + } + public XiaomiActivityFileFetcher getActivityFetcher() { return activityFetcher; } diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto index f078be612..d8325375a 100644 --- a/app/src/main/proto/xiaomi.proto +++ b/app/src/main/proto/xiaomi.proto @@ -392,7 +392,11 @@ message VitalityScore { message WorkoutStatusWatch { optional uint32 timestamp = 1; // seconds - optional uint32 unknown2 = 2; + optional uint32 sport = 3; + optional uint32 status = 4; // 0 started, 1 resumed, 2 paused, 3 finished + optional bytes activityFileIds = 5; + optional uint32 unknown6 = 6; // 2 + optional uint32 unknown10 = 10; // 0 } message WorkoutOpenWatch { @@ -404,7 +408,7 @@ message WorkoutOpenWatch { message WorkoutOpenReply { // 3 2 10 when no gps permissions at all - // 5 2 10 when no background permissions? + // 5 2 10 when no all time gps permission // ... // 0 * * when phone gps is working fine // 0 2 10 @@ -415,14 +419,15 @@ message WorkoutOpenReply { } message WorkoutLocation { - optional uint32 unknown1 = 1; // 10, sometimes 2 + optional uint32 numSatellites = 1; // 10, sometimes 2? optional uint32 timestamp = 2; // seconds optional double longitude = 3; optional double latitude = 4; - optional float unknown6 = 6; // ? - optional float unknown7 = 7; // altitude? - optional float unknown8 = 8; // ? - optional float unknown9 = 9; // ? + optional double altitude = 5; + optional float speed = 6; + optional float bearing = 7; + optional float horizontalAccuracy = 8; + optional float verticalAccuracy = 9; } message RealTimeStats {