1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2025-01-12 10:55:49 +01:00

Mi Band 5: Send GPS location to band during workout

This commit is contained in:
José Rebelo 2022-06-04 21:20:28 +01:00 committed by vanous
parent ee93cce16d
commit b07cd54468
29 changed files with 830 additions and 114 deletions

View File

@ -84,6 +84,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver; import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -527,6 +528,14 @@ public class DebugActivity extends AbstractGBActivity {
} }
}); });
Button stopPhoneGpsLocationListener = findViewById(R.id.stopPhoneGpsLocationListener);
stopPhoneGpsLocationListener.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GBLocationManager.stopAll(getBaseContext());
}
});
Button showStatusFitnessAppTracking = findViewById(R.id.showStatusFitnessAppTracking); Button showStatusFitnessAppTracking = findViewById(R.id.showStatusFitnessAppTracking);
final int delay = 2 * 1000; final int delay = 2 * 1000;

View File

@ -116,6 +116,8 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_DO_NOT_DISTURB_AUTOMATIC = "automatic"; public static final String PREF_DO_NOT_DISTURB_AUTOMATIC = "automatic";
public static final String PREF_DO_NOT_DISTURB_SCHEDULED = "scheduled"; public static final String PREF_DO_NOT_DISTURB_SCHEDULED = "scheduled";
public static final String PREF_WORKOUT_SEND_GPS_TO_BAND = "workout_send_gps_to_band";
public static final String PREF_FIND_PHONE = "prefs_find_phone"; public static final String PREF_FIND_PHONE = "prefs_find_phone";
public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration"; public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration";
public static final String PREF_AUTOLIGHT = "autolight"; public static final String PREF_AUTOLIGHT = "autolight";

View File

@ -18,6 +18,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices; package nodomain.freeyourgadget.gadgetbridge.devices;
import android.location.Location;
import android.net.Uri; import android.net.Uri;
import java.util.ArrayList; import java.util.ArrayList;
@ -130,4 +131,6 @@ public interface EventHandler {
void onSetLedColor(int color); void onSetLedColor(int color);
void onPowerOff(); void onPowerOff();
void onSetGpsLocation(Location location);
} }

View File

@ -363,6 +363,12 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator {
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_LIFT_WRIST, false); return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_LIFT_WRIST, false);
} }
public static boolean getWorkoutSendGpsToBand(String deviceAddress) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false);
}
@Override @Override
public boolean supportsScreenshots() { public boolean supportsScreenshots() {
return false; return false;

View File

@ -35,8 +35,9 @@ public class HuamiService {
public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString("00001532-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString("00001532-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC0 = UUID.fromString("00000000-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC0 = UUID.fromString("00000000-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC1 = UUID.fromString("00000001-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_RAW_SENSOR_CONTROL = UUID.fromString("00000001-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC2 = UUID.fromString("00000002-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_RAW_SENSOR_DATA = UUID.fromString("00000002-0000-3512-2118-0009af100700");
/** /**
* Alarms, Display and other configuration. * Alarms, Display and other configuration.
*/ */
@ -48,9 +49,11 @@ public class HuamiService {
public static final UUID UUID_CHARACTERISTIC_8_USER_SETTINGS = UUID.fromString("00000008-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_8_USER_SETTINGS = UUID.fromString("00000008-0000-3512-2118-0009af100700");
// service uuid fee1 // service uuid fee1
public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_WORKOUT = UUID.fromString("0000000f-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_DEVICEEVENT = UUID.fromString("00000010-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_DEVICEEVENT = UUID.fromString("00000010-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_AUDIO = UUID.fromString("00000012-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_AUDIO = UUID.fromString("00000012-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_AUDIODATA = UUID.fromString("00000013-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_AUDIODATA = UUID.fromString("00000013-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC5 = UUID.fromString("00000014-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE = UUID.fromString("00000016-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE = UUID.fromString("00000016-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ = UUID.fromString("00000017-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ = UUID.fromString("00000017-0000-3512-2118-0009af100700");

View File

@ -115,6 +115,7 @@ public class MiBand5Coordinator extends HuamiCoordinator {
R.xml.devicesettings_nightmode, R.xml.devicesettings_nightmode,
R.xml.devicesettings_liftwrist_display_sensitivity, R.xml.devicesettings_liftwrist_display_sensitivity,
R.xml.devicesettings_inactivity_dnd, R.xml.devicesettings_inactivity_dnd,
R.xml.devicesettings_workout_send_gps_to_band,
R.xml.devicesettings_swipeunlock, R.xml.devicesettings_swipeunlock,
R.xml.devicesettings_sync_calendar, R.xml.devicesettings_sync_calendar,
R.xml.devicesettings_reserve_reminders_calendar, R.xml.devicesettings_reserve_reminders_calendar,

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.content.Context;
import android.location.LocationListener;
/**
* An abstract location provider, which periodically sends a location update to the provided {@link LocationListener}.
*/
public abstract class AbstractLocationProvider {
private final LocationListener locationListener;
public AbstractLocationProvider(final LocationListener locationListener) {
this.locationListener = locationListener;
}
protected final LocationListener getLocationListener() {
return this.locationListener;
}
/**
* Start sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void start(final Context context);
/**
* Stop sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void stop(final Context context);
}

View File

@ -0,0 +1,80 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* An implementation of a {@link LocationListener} that forwards the location updates to the
* provided {@link EventHandler}.
*/
public class GBLocationListener implements LocationListener {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationListener.class);
private final EventHandler eventHandler;
private Location previousLocation;
public GBLocationListener(final EventHandler eventHandler) {
this.eventHandler = eventHandler;
}
@Override
public void onLocationChanged(final Location location) {
LOG.info("Location changed: {}", location);
// The location usually doesn't contain speed, compute it from the previous location
if (previousLocation != null && !location.hasSpeed()) {
long timeInterval = (location.getTime() - previousLocation.getTime()) / 1000L;
float distanceInMeters = previousLocation.distanceTo(location);
location.setSpeed(distanceInMeters / timeInterval);
}
previousLocation = location;
eventHandler.onSetGpsLocation(location);
}
@Override
public void onProviderDisabled(final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}
@Override
public void onProviderEnabled(final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}
@Override
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
LOG.info("onStatusChanged: {}", provider, status);
}
}

View File

@ -0,0 +1,86 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A static location manager, which keeps track of what providers are currently running. A notification is kept
* while there is at least one provider runnin.
*/
public class GBLocationManager {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationManager.class);
/**
* The current number of running listeners.
*/
private static Map<EventHandler, AbstractLocationProvider> providers = new HashMap<>();
public static void start(final Context context, final EventHandler eventHandler) {
if (providers.containsKey(eventHandler)) {
LOG.warn("EventHandler already registered");
return;
}
GB.createGpsNotification(context, providers.size() + 1);
final GBLocationListener locationListener = new GBLocationListener(eventHandler);
final AbstractLocationProvider locationProvider = new PhoneGpsLocationProvider(locationListener);
locationProvider.start(context);
providers.put(eventHandler, locationProvider);
}
public static void stop(final Context context, final EventHandler eventHandler) {
final AbstractLocationProvider locationProvider = providers.remove(eventHandler);
if (locationProvider != null) {
LOG.warn("EventHandler not registered");
locationProvider.stop(context);
}
if (!providers.isEmpty()) {
GB.createGpsNotification(context, providers.size());
} else {
GB.removeGpsNotification(context);
}
}
public static void stopAll(final Context context) {
for (EventHandler eventHandler : providers.keySet()) {
stop(context, eventHandler);
}
}
}

View File

@ -0,0 +1,96 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.os.Handler;
import android.os.Looper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
/**
* A mock location provider which keeps updating the location at a constant speed, starting from the
* last known location. Useful for local tests.
*/
public class MockLocationProvider extends AbstractLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(MockLocationProvider.class);
private Location previousLocation = new CurrentPosition().getLastKnownLocation();
/**
* Interval between location updates, in milliseconds.
*/
private final int interval = 1000;
/**
* Difference between location updates, in degrees.
*/
private final float coordDiff = 0.0002f;
/**
* Whether the handler is running.
*/
private boolean running = false;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable locationUpdateRunnable = new Runnable() {
@Override
public void run() {
if (!running) {
return;
}
final Location newLocation = new Location(previousLocation);
newLocation.setLatitude(previousLocation.getLatitude() + coordDiff);
newLocation.setTime(System.currentTimeMillis());
getLocationListener().onLocationChanged(newLocation);
previousLocation = newLocation;
if (running) {
handler.postDelayed(this, interval);
}
}
};
public MockLocationProvider(LocationListener locationListener) {
super(locationListener);
}
@Override
void start(final Context context) {
LOG.info("Starting mock location provider");
running = true;
handler.postDelayed(locationUpdateRunnable, interval);
}
@Override
void stop(final Context context) {
LOG.info("Stopping mock location provider");
running = false;
handler.removeCallbacksAndMessages(null);
}
}

View File

@ -0,0 +1,66 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Looper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A location provider that uses the phone GPS, using {@link LocationManager}.
*/
public class PhoneGpsLocationProvider extends AbstractLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneGpsLocationProvider.class);
private static final int INTERVAL_MIN_TIME = 1000;
private static final int INTERVAL_MIN_DISTANCE = 0;
public PhoneGpsLocationProvider(LocationListener locationListener) {
super(locationListener);
}
@Override
void start(final Context context) {
LOG.info("Starting phone gps location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
INTERVAL_MIN_TIME,
INTERVAL_MIN_DISTANCE,
getLocationListener(),
Looper.getMainLooper()
);
final Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
LOG.debug("Last known location: {}", lastKnownLocation);
}
@Override
void stop(final Context context) {
LOG.info("Stopping phone gps location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
}
}

View File

@ -23,6 +23,7 @@ import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.location.Location;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.ContactsContract; import android.provider.ContactsContract;
@ -471,4 +472,11 @@ public class GBDeviceService implements DeviceService {
Intent intent = createIntent().setAction(ACTION_POWER_OFF); Intent intent = createIntent().setAction(ACTION_POWER_OFF);
invokeService(intent); invokeService(intent);
} }
@Override
public void onSetGpsLocation(Location location) {
Intent intent = createIntent().setAction(ACTION_SET_GPS_LOCATION);
intent.putExtra(EXTRA_GPS_LOCATION, location);
invokeService(intent);
}
} }

View File

@ -70,6 +70,7 @@ public interface DeviceService extends EventHandler {
String ACTION_SEND_WEATHER = PREFIX + ".action.send_weather"; String ACTION_SEND_WEATHER = PREFIX + ".action.send_weather";
String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function"; String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function";
String ACTION_SET_FM_FREQUENCY = PREFIX + ".action.set_fm_frequency"; String ACTION_SET_FM_FREQUENCY = PREFIX + ".action.set_fm_frequency";
String ACTION_SET_GPS_LOCATION = PREFIX + ".action.set_gps_location";
String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color"; String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color";
String ACTION_POWER_OFF = PREFIX + ".action.power_off"; String ACTION_POWER_OFF = PREFIX + ".action.power_off";
String EXTRA_NOTIFICATION_BODY = "notification_body"; String EXTRA_NOTIFICATION_BODY = "notification_body";
@ -122,6 +123,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_RECORDED_DATA_TYPES = "data_types"; String EXTRA_RECORDED_DATA_TYPES = "data_types";
String EXTRA_FM_FREQUENCY = "fm_frequency"; String EXTRA_FM_FREQUENCY = "fm_frequency";
String EXTRA_LED_COLOR = "led_color"; String EXTRA_LED_COLOR = "led_color";
String EXTRA_GPS_LOCATION = "gps_location";
String EXTRA_RESET_FLAGS = "reset_flags"; String EXTRA_RESET_FLAGS = "reset_flags";
/** /**

View File

@ -30,6 +30,7 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.location.Location;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
@ -86,104 +87,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.LanguageUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TRANSLITERATION_ENABLED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TRANSLITERATION_ENABLED;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ADD_CALENDAREVENT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.*;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_CONFIGURE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_REORDER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CALLSTATE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETE_CALENDAREVENT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETE_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DISCONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_REALTIME_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FETCH_RECORDED_DATA;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FIND_DEVICE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_HEARTRATE_TEST;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_INSTALL;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_POWER_OFF;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_READ_CONFIGURATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RESET;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_CONFIGURATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_WEATHER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETCANNEDMESSAGES;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICSTATE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONSTANT_VIBRATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_FM_FREQUENCY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_HEARTRATE_MEASUREMENT_INTERVAL;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_LED_COLOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_PHONE_VOLUME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_REMINDERS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_WORLD_CLOCKS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_ALARMS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_CONFIG;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_CONFIG_ID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_UUID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_BOOLEAN_ENABLE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_DESCRIPTION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_ID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_LOCATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TITLE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_DISPLAYNAME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_DNDSUPPRESSED;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_PHONENUMBER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONNECT_FIRST_TIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FM_FREQUENCY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_INTERVAL_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_LED_COLOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_POSITION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_RATE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_REPEAT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_SHUFFLE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_STATE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACK;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKCOUNT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKNR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ACTIONS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_DNDSUPPRESSED;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_FLAGS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ICONID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PEBBLE_COLOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PHONENUMBER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SENDER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SOURCEAPPID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SOURCENAME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SUBJECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TITLE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_PHONE_VOLUME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RECORDED_DATA_TYPES;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_REMINDERS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RESET_FLAGS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_VIBRATION_INTENSITY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WORLD_CLOCKS;
public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener { public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class); private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class);
@ -657,6 +561,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mDeviceSupport.onSetFmFrequency(frequency); mDeviceSupport.onSetFmFrequency(frequency);
} }
break; break;
case ACTION_SET_GPS_LOCATION:
final Location location = intent.getParcelableExtra(EXTRA_GPS_LOCATION);
mDeviceSupport.onSetGpsLocation(location);
break;
} }
} }

View File

@ -20,6 +20,7 @@ package nodomain.freeyourgadget.gadgetbridge.service;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.content.Context; import android.content.Context;
import android.location.Location;
import android.net.Uri; import android.net.Uri;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -438,4 +439,12 @@ public class ServiceDeviceSupport implements DeviceSupport {
} }
delegate.onPowerOff(); delegate.onPowerOff();
} }
@Override
public void onSetGpsLocation(Location location) {
if (checkBusy("set gps location")) {
return;
}
delegate.onSetGpsLocation(location);
}
} }

View File

@ -23,6 +23,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothGattService;
import android.content.Intent; import android.content.Intent;
import android.location.Location;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -379,6 +380,11 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
} }
@Override
public void onSetGpsLocation(Location location) {
}
@Override @Override
public void onSetReminders(ArrayList<? extends Reminder> reminders) { public void onSetReminders(ArrayList<? extends Reminder> reminders) {

View File

@ -31,6 +31,7 @@ public class HuamiDeviceEvent {
public static final byte TICK_30MIN = 0x0e; // unsure public static final byte TICK_30MIN = 0x0e; // unsure
public static final byte FIND_PHONE_STOP = 0x0f; public static final byte FIND_PHONE_STOP = 0x0f;
public static final byte MTU_REQUEST = 0x16; public static final byte MTU_REQUEST = 0x16;
public static final byte WORKOUT_STARTING = 0x14;
public static final byte ALARM_CHANGED = 0x1a; public static final byte ALARM_CHANGED = 0x1a;
public static final byte MUSIC_CONTROL = (byte) 0xfe; public static final byte MUSIC_CONTROL = (byte) 0xfe;
} }

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
/**
* The phone GPS status, to signal the band.
*/
public enum HuamiPhoneGpsStatus {
ACQUIRED(0x01),
SEARCHING(0x02),
DISABLED(0x04),
;
private final byte code;
HuamiPhoneGpsStatus(final int code) {
this.code = (byte) code;
}
public byte getCode() {
return code;
}
public static HuamiPhoneGpsStatus fromCode(final byte code) {
for (final HuamiPhoneGpsStatus type : values()) {
if (type.getCode() == code) {
return type;
}
}
return null;
}
}

View File

@ -26,6 +26,7 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.location.Location;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.widget.Toast; import android.widget.Toast;
@ -102,6 +103,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -458,6 +460,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIO), enable); builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIO), enable);
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIODATA), enable); builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIODATA), enable);
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT), enable); builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT), enable);
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_WORKOUT), enable);
if (characteristicChunked2021Read != null) { if (characteristicChunked2021Read != null) {
builder.notify(characteristicChunked2021Read, enable); builder.notify(characteristicChunked2021Read, enable);
} }
@ -1848,19 +1851,169 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
requestMTU(mtu); requestMTU(mtu);
} }
*/ */
break;
case HuamiDeviceEvent.WORKOUT_STARTING:
final HuamiWorkoutTrackActivityType activityType = HuamiWorkoutTrackActivityType.fromCode(value[3]);
this.workoutNeedsGps = (value[2] == 1);
if (activityType == null) {
LOG.warn("Unknown workout activity type {}", String.format("0x%x", value[3]));
}
LOG.info("Workout starting on band: {}, needs gps = {}", activityType, workoutNeedsGps);
final boolean sendGpsToBand = HuamiCoordinator.getWorkoutSendGpsToBand(getDevice().getAddress());
if (workoutNeedsGps) {
if (sendGpsToBand) {
lastPhoneGpsSent = 0;
sendPhoneGpsStatus(HuamiPhoneGpsStatus.SEARCHING);
GBLocationManager.start(getContext(), this);
} else {
sendPhoneGpsStatus(HuamiPhoneGpsStatus.DISABLED);
}
}
break; break;
default: default:
LOG.warn("unhandled event " + value[0]); LOG.warn("unhandled event {}", String.format("0x%x", value[0]));
} }
} }
private void requestMTU(int mtu) { /**
if (GBApplication.isRunningLollipopOrLater()) { * Track whether the currently selected workout needs gps (received in {@link #handleDeviceEvent}, so we can start the activity tracking
new TransactionBuilder("requestMtu") * if needed in {@link #handleDeviceWorkoutEvent}, since in there we don't know what's the current workout.
.requestMtu(mtu) */
.queue(getQueue()); private boolean workoutNeedsGps = false;
mMTU = mtu;
/**
* Track the last time we actually sent a gps location. We need to signal that GPS as re-acquired if the last update was too long ago.
*/
private long lastPhoneGpsSent = 0;
private void handleDeviceWorkoutEvent(byte[] value) {
if (value == null || value.length == 0) {
return;
} }
switch (value[0]) {
case 0x11:
final HuamiWorkoutStatus status = HuamiWorkoutStatus.fromCode(value[1]);
if (status == null) {
LOG.warn("Unknown workout status {}", String.format("0x%x", value[1]));
return;
}
LOG.info("Got workout status {}", status);
final boolean sendGpsToBand = HuamiCoordinator.getWorkoutSendGpsToBand(getDevice().getAddress());
switch (status) {
case Start:
break;
case End:
GBLocationManager.stop(getContext(), this);
break;
}
break;
default:
LOG.warn("Unhandled workout event {}", String.format("0x%x", value[0]));
}
}
@Override
public void onSetGpsLocation(final Location location) {
if (characteristicChunked == null || location == null) {
return;
}
final boolean sendGpsToBand = HuamiCoordinator.getWorkoutSendGpsToBand(getDevice().getAddress());
if (!sendGpsToBand) {
LOG.warn("Sending GPS to band is disabled, ignoring location update");
return;
}
int flags = 0x40000;
int length = 1 + 4 + 31;
boolean newGpsLock = System.currentTimeMillis() - lastPhoneGpsSent > 5000;
lastPhoneGpsSent = System.currentTimeMillis();
if (newGpsLock) {
flags |= 0x01;
length += 1;
}
final ByteBuffer buf = ByteBuffer.allocate(length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 0x06);
buf.putInt(flags);
if (newGpsLock) {
buf.put((byte) 0x01);
}
buf.putInt((int) (location.getLongitude() * 3000000.0));
buf.putInt((int) (location.getLatitude() * 3000000.0));
buf.putInt((int) location.getSpeed() * 10);
buf.putInt((int) (location.getAltitude() * 100));
buf.putLong(location.getTime());
// Seems to always be ff ?
buf.putInt(0xffffffff);
// Not sure what this is, maybe bearing? It changes while moving, but
// doesn't seem to be needed on the Mi Band 5
buf.putShort((short) 0x00);
// Seems to always be 0 ?
buf.put((byte) 0x00);
try {
final TransactionBuilder builder = performInitialized("send phone gps location");
writeToChunked(builder, 6, buf.array());
builder.queue(getQueue());
} catch (final IOException e) {
LOG.error("Unable to send location", e);
}
LOG.info("sendLocationToBand: {}", location);
}
private void sendPhoneGpsStatus(final HuamiPhoneGpsStatus status) {
int flags = 0x01;
final ByteBuffer buf = ByteBuffer.allocate(1 + 4 + 1);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 0x06);
buf.putInt(flags);
buf.put(status.getCode());
try {
final TransactionBuilder builder = performInitialized("send phone gps status");
writeToChunked(builder, 6, buf.array());
builder.queue(getQueue());
} catch (final IOException e) {
LOG.error("Unable to send location", e);
}
LOG.info("sendPhoneGpsStatus: {}", status);
}
private void requestMTU(int mtu) {
if (!GBApplication.isRunningLollipopOrLater()) {
LOG.warn("Requesting MTU is only supported in Lollipop or later");
return;
}
new TransactionBuilder("requestMtu")
.requestMtu(mtu)
.queue(getQueue());
mMTU = mtu;
} }
private void acknowledgeFindPhone() { private void acknowledgeFindPhone() {
@ -1985,6 +2138,9 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
} else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) { } else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) {
handleDeviceEvent(characteristic.getValue()); handleDeviceEvent(characteristic.getValue());
return true; return true;
} else if (HuamiService.UUID_CHARACTERISTIC_WORKOUT.equals(characteristicUUID)) {
handleDeviceWorkoutEvent(characteristic.getValue());
return true;
} else if (HuamiService.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) { } else if (HuamiService.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) {
handleRealtimeSteps(characteristic.getValue()); handleRealtimeSteps(characteristic.getValue());
return true; return true;
@ -2026,6 +2182,9 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
} else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) { } else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) {
handleDeviceEvent(characteristic.getValue()); handleDeviceEvent(characteristic.getValue());
return true; return true;
} else if (HuamiService.UUID_CHARACTERISTIC_WORKOUT.equals(characteristicUUID)) {
handleDeviceWorkoutEvent(characteristic.getValue());
return true;
} else { } else {
LOG.info("Unhandled characteristic read: " + characteristicUUID); LOG.info("Unhandled characteristic read: " + characteristicUUID);
logMessageContent(characteristic.getValue()); logMessageContent(characteristic.getValue());
@ -3204,7 +3363,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
int pos = 2; int pos = 2;
for (final String workoutType : enabledActivityTypes) { for (final String workoutType : enabledActivityTypes) {
command[pos++] = HuamiWorkoutActivityType.fromPrefValue(workoutType).getCode(); command[pos++] = HuamiWorkoutScreenActivityType.fromPrefValue(workoutType).getCode();
command[pos++] = 0x00; command[pos++] = 0x00;
command[pos++] = 0x01; command[pos++] = 0x01;
} }
@ -3212,7 +3371,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
// Send all the remaining disabled workout types // Send all the remaining disabled workout types
for (final String workoutType : allActivityTypes) { for (final String workoutType : allActivityTypes) {
if (!enabledActivityTypes.contains(workoutType)) { if (!enabledActivityTypes.contains(workoutType)) {
command[pos++] = HuamiWorkoutActivityType.fromPrefValue(workoutType).getCode(); command[pos++] = HuamiWorkoutScreenActivityType.fromPrefValue(workoutType).getCode();
command[pos++] = 0x00; command[pos++] = 0x00;
command[pos++] = 0x00; command[pos++] = 0x00;
} }

View File

@ -18,7 +18,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
import java.util.Locale; import java.util.Locale;
public enum HuamiWorkoutActivityType { /**
* The workout types, to configure the workouts screen on the band.
*/
public enum HuamiWorkoutScreenActivityType {
OutdoorRunning(0x01), OutdoorRunning(0x01),
Walking(0x06), Walking(0x06),
Treadmill(0x08), Treadmill(0x08),
@ -33,7 +36,7 @@ public enum HuamiWorkoutActivityType {
private final byte code; private final byte code;
HuamiWorkoutActivityType(final int code) { HuamiWorkoutScreenActivityType(final int code) {
this.code = (byte) code; this.code = (byte) code;
} }
@ -41,12 +44,12 @@ public enum HuamiWorkoutActivityType {
return code; return code;
} }
public static HuamiWorkoutActivityType fromPrefValue(final String prefValue) { public static HuamiWorkoutScreenActivityType fromPrefValue(final String prefValue) {
for (HuamiWorkoutActivityType type : values()) { for (final HuamiWorkoutScreenActivityType type : values()) {
if (type.name().toLowerCase(Locale.ROOT).equals(prefValue.replace("_", "").toLowerCase(Locale.ROOT))) { if (type.name().toLowerCase(Locale.ROOT).equals(prefValue.replace("_", "").toLowerCase(Locale.ROOT))) {
return type; return type;
} }
} }
throw new RuntimeException("No matching HuamiWorkoutActivityType for pref value: " + prefValue); throw new RuntimeException("No matching HuamiWorkoutScreenActivityType for pref value: " + prefValue);
} }
} }

View File

@ -0,0 +1,45 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
public enum HuamiWorkoutStatus {
Start(0x02),
Pause(0x03),
Resume(0x04),
End(0x05),
;
private final byte code;
HuamiWorkoutStatus(final int code) {
this.code = (byte) code;
}
public byte getCode() {
return code;
}
public static HuamiWorkoutStatus fromCode(final byte code) {
for (final HuamiWorkoutStatus type : values()) {
if (type.getCode() == code) {
return type;
}
}
return null;
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2022 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
import java.util.Locale;
/**
* The workout types, used to start / when workout tracking starts on the band.
*/
public enum HuamiWorkoutTrackActivityType {
OutdoorRunning(0x01),
Walking(0x04),
Treadmill(0x02),
OutdoorCycling(0x03),
IndoorCycling(0x09),
Elliptical(0x06),
PoolSwimming(0x05),
Freestyle(0x0b),
JumpRope(0x08),
RowingMachine(0x07),
Yoga(0x0a);
private final byte code;
HuamiWorkoutTrackActivityType(final int code) {
this.code = (byte) code;
}
public byte getCode() {
return code;
}
public static HuamiWorkoutTrackActivityType fromCode(final byte code) {
for (final HuamiWorkoutTrackActivityType type : values()) {
if (type.getCode() == code) {
return type;
}
}
return null;
}
}

View File

@ -17,6 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.serial; package nodomain.freeyourgadget.gadgetbridge.service.serial;
import android.location.Location;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.UUID; import java.util.UUID;
@ -289,4 +291,10 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport
byte[] bytes = gbDeviceProtocol.encodeWorldClocks(clocks); byte[] bytes = gbDeviceProtocol.encodeWorldClocks(clocks);
sendToDevice(bytes); sendToDevice(bytes);
} }
@Override
public void onSetGpsLocation(Location location) {
byte[] bytes = gbDeviceProtocol.encodeGpsLocation(location);
sendToDevice(bytes);
}
} }

View File

@ -17,6 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.serial; package nodomain.freeyourgadget.gadgetbridge.service.serial;
import android.location.Location;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.UUID; import java.util.UUID;
@ -163,4 +165,8 @@ public abstract class GBDeviceProtocol {
public byte[] encodeFmFrequency(float frequency) { public byte[] encodeFmFrequency(float frequency) {
return null; return null;
} }
public byte[] encodeGpsLocation(Location location) {
return null;
}
} }

View File

@ -65,6 +65,7 @@ public class GB {
public static final String NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID = "gadgetbridge_high_priority"; public static final String NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID = "gadgetbridge_high_priority";
public static final String NOTIFICATION_CHANNEL_ID_TRANSFER = "gadgetbridge transfer"; public static final String NOTIFICATION_CHANNEL_ID_TRANSFER = "gadgetbridge transfer";
public static final String NOTIFICATION_CHANNEL_ID_LOW_BATTERY = "low_battery"; public static final String NOTIFICATION_CHANNEL_ID_LOW_BATTERY = "low_battery";
public static final String NOTIFICATION_CHANNEL_ID_GPS = "gps";
public static final int NOTIFICATION_ID = 1; public static final int NOTIFICATION_ID = 1;
public static final int NOTIFICATION_ID_INSTALL = 2; public static final int NOTIFICATION_ID_INSTALL = 2;
@ -72,6 +73,7 @@ public class GB {
public static final int NOTIFICATION_ID_TRANSFER = 4; public static final int NOTIFICATION_ID_TRANSFER = 4;
public static final int NOTIFICATION_ID_EXPORT_FAILED = 5; public static final int NOTIFICATION_ID_EXPORT_FAILED = 5;
public static final int NOTIFICATION_ID_PHONE_FIND = 6; public static final int NOTIFICATION_ID_PHONE_FIND = 6;
public static final int NOTIFICATION_ID_GPS = 7;
public static final int NOTIFICATION_ID_ERROR = 42; public static final int NOTIFICATION_ID_ERROR = 42;
private static final Logger LOG = LoggerFactory.getLogger(GB.class); private static final Logger LOG = LoggerFactory.getLogger(GB.class);
@ -122,6 +124,12 @@ public class GB {
context.getString(R.string.notification_channel_low_battery_name), context.getString(R.string.notification_channel_low_battery_name),
NotificationManager.IMPORTANCE_DEFAULT); NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channelLowBattery); notificationManager.createNotificationChannel(channelLowBattery);
NotificationChannel channelGps = new NotificationChannel(
NOTIFICATION_CHANNEL_ID_GPS,
context.getString(R.string.notification_channel_gps),
NotificationManager.IMPORTANCE_MIN);
notificationManager.createNotificationChannel(channelGps);
} }
notificationChannelsCreated = true; notificationChannelsCreated = true;
@ -440,6 +448,27 @@ public class GB {
} }
} }
public static void createGpsNotification(Context context, int numDevices) {
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_GPS)
.setTicker(context.getString(R.string.notification_gps_title))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.notification_gps_title))
.setContentText(context.getString(R.string.notification_gps_text, numDevices))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_gps_location)
.setOngoing(true);
notify(NOTIFICATION_ID_GPS, nb.build(), context);
}
public static void removeGpsNotification(Context context) {
removeNotification(NOTIFICATION_ID_GPS, context);
}
private static Notification createInstallNotification(String text, boolean ongoing, private static Notification createInstallNotification(String text, boolean ongoing,
int percentage, Context context) { int percentage, Context context) {
Intent notificationIntent = new Intent(context, ControlCenterv2.class); Intent notificationIntent = new Intent(context, ControlCenterv2.class);

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#7E7E7E"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View File

@ -256,6 +256,14 @@
android:text="Show Fit.App.Track. Status" android:text="Show Fit.App.Track. Status"
grid:layout_columnSpan="2" grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" /> grid:layout_gravity="fill_horizontal" />
<Button
android:id="@+id/stopPhoneGpsLocationListener"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pref_device_action_phone_gps_location_listener_stop"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" />
</androidx.gridlayout.widget.GridLayout> </androidx.gridlayout.widget.GridLayout>
</ScrollView> </ScrollView>

View File

@ -370,6 +370,8 @@
<string name="prefs_hr_alarm_activity">Heart rate alarm during sports activity</string> <string name="prefs_hr_alarm_activity">Heart rate alarm during sports activity</string>
<string name="prefs_hr_alarm_low">Low limit</string> <string name="prefs_hr_alarm_low">Low limit</string>
<string name="prefs_hr_alarm_high">High limit</string> <string name="prefs_hr_alarm_high">High limit</string>
<string name="pref_workout_send_gps_title">Send GPS during workout</string>
<string name="pref_workout_send_gps_summary">Send the current GPS location to the band during a workout</string>
<!-- Auto export preferences --> <!-- Auto export preferences -->
<string name="pref_header_auto_export">Auto export</string> <string name="pref_header_auto_export">Auto export</string>
<string name="pref_title_auto_export_enabled">Auto export enabled</string> <string name="pref_title_auto_export_enabled">Auto export enabled</string>
@ -1029,6 +1031,9 @@
<string name="notification_channel_high_priority_name">High-priority</string> <string name="notification_channel_high_priority_name">High-priority</string>
<string name="notification_channel_transfer_name">Data transfer</string> <string name="notification_channel_transfer_name">Data transfer</string>
<string name="notification_channel_low_battery_name">Low battery</string> <string name="notification_channel_low_battery_name">Low battery</string>
<string name="notification_channel_gps">GPS tracking</string>
<string name="notification_gps_title">Gadgetbridge GPS</string>
<string name="notification_gps_text">Sending GPS location to %1$d devices</string>
<string name="devicetype_amazfit_gts">Amazfit GTS</string> <string name="devicetype_amazfit_gts">Amazfit GTS</string>
<string name="devicetype_amazfit_vergel">Amazfit Verge Lite</string> <string name="devicetype_amazfit_vergel">Amazfit Verge Lite</string>
<string name="devicetype_sg2">Lemfo SG2</string> <string name="devicetype_sg2">Lemfo SG2</string>
@ -1624,6 +1629,7 @@
<string name="pref_device_action_fitness_app_control_start">Fitness App Tracking Start</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> <string name="pref_device_action_fitness_app_control_stop">Fitness App Tracking Stop</string>
<string name="pref_device_action_fitness_app_control_toggle">Toggle Fitness App Tracking</string> <string name="pref_device_action_fitness_app_control_toggle">Toggle Fitness App Tracking</string>
<string name="pref_device_action_phone_gps_location_listener_stop">GPS Location Listener Stop</string>
<!-- Translators: the ### indicate number of digits, keep intact --> <!-- Translators: the ### indicate number of digits, keep intact -->
<string name="distance_format_meters">###m</string> <string name="distance_format_meters">###m</string>
<!-- Translators: the ### indicate number of digits, keep intact --> <!-- Translators: the ### indicate number of digits, keep intact -->

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_gps_location"
android:key="workout_send_gps_to_band"
android:summary="@string/pref_workout_send_gps_summary"
android:title="@string/pref_workout_send_gps_title" />
</androidx.preference.PreferenceScreen>