mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-12 02:45:49 +01:00
Mi Band 5: Send GPS location to band during workout
This commit is contained in:
parent
ee93cce16d
commit
b07cd54468
@ -84,6 +84,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
|
||||
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);
|
||||
final int delay = 2 * 1000;
|
||||
|
||||
|
@ -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_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_DURATION = "prefs_find_phone_duration";
|
||||
public static final String PREF_AUTOLIGHT = "autolight";
|
||||
|
@ -18,6 +18,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -130,4 +131,6 @@ public interface EventHandler {
|
||||
void onSetLedColor(int color);
|
||||
|
||||
void onPowerOff();
|
||||
|
||||
void onSetGpsLocation(Location location);
|
||||
}
|
||||
|
@ -363,6 +363,12 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator {
|
||||
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
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
|
@ -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_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_CHARACTERISTIC2 = UUID.fromString("00000002-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_RAW_SENSOR_DATA = UUID.fromString("00000002-0000-3512-2118-0009af100700");
|
||||
|
||||
/**
|
||||
* 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");
|
||||
// service uuid fee1
|
||||
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_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_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_READ = UUID.fromString("00000017-0000-3512-2118-0009af100700");
|
||||
|
@ -115,6 +115,7 @@ public class MiBand5Coordinator extends HuamiCoordinator {
|
||||
R.xml.devicesettings_nightmode,
|
||||
R.xml.devicesettings_liftwrist_display_sensitivity,
|
||||
R.xml.devicesettings_inactivity_dnd,
|
||||
R.xml.devicesettings_workout_send_gps_to_band,
|
||||
R.xml.devicesettings_swipeunlock,
|
||||
R.xml.devicesettings_sync_calendar,
|
||||
R.xml.devicesettings_reserve_reminders_calendar,
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.ContactsContract;
|
||||
@ -471,4 +472,11 @@ public class GBDeviceService implements DeviceService {
|
||||
Intent intent = createIntent().setAction(ACTION_POWER_OFF);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetGpsLocation(Location location) {
|
||||
Intent intent = createIntent().setAction(ACTION_SET_GPS_LOCATION);
|
||||
intent.putExtra(EXTRA_GPS_LOCATION, location);
|
||||
invokeService(intent);
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ public interface DeviceService extends EventHandler {
|
||||
String ACTION_SEND_WEATHER = PREFIX + ".action.send_weather";
|
||||
String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function";
|
||||
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_POWER_OFF = PREFIX + ".action.power_off";
|
||||
String EXTRA_NOTIFICATION_BODY = "notification_body";
|
||||
@ -122,6 +123,7 @@ public interface DeviceService extends EventHandler {
|
||||
String EXTRA_RECORDED_DATA_TYPES = "data_types";
|
||||
String EXTRA_FM_FREQUENCY = "fm_frequency";
|
||||
String EXTRA_LED_COLOR = "led_color";
|
||||
String EXTRA_GPS_LOCATION = "gps_location";
|
||||
String EXTRA_RESET_FLAGS = "reset_flags";
|
||||
|
||||
/**
|
||||
|
@ -30,6 +30,7 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
@ -86,104 +87,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.LanguageUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
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.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;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.*;
|
||||
|
||||
public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class);
|
||||
@ -657,6 +561,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
mDeviceSupport.onSetFmFrequency(frequency);
|
||||
}
|
||||
break;
|
||||
case ACTION_SET_GPS_LOCATION:
|
||||
final Location location = intent.getParcelableExtra(EXTRA_GPS_LOCATION);
|
||||
mDeviceSupport.onSetGpsLocation(location);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ package nodomain.freeyourgadget.gadgetbridge.service;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -438,4 +439,12 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
}
|
||||
delegate.onPowerOff();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetGpsLocation(Location location) {
|
||||
if (checkBusy("set gps location")) {
|
||||
return;
|
||||
}
|
||||
delegate.onSetGpsLocation(location);
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
@ -379,6 +380,11 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetGpsLocation(Location location) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetReminders(ArrayList<? extends Reminder> reminders) {
|
||||
|
||||
|
@ -31,6 +31,7 @@ public class HuamiDeviceEvent {
|
||||
public static final byte TICK_30MIN = 0x0e; // unsure
|
||||
public static final byte FIND_PHONE_STOP = 0x0f;
|
||||
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 MUSIC_CONTROL = (byte) 0xfe;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
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.MiBandActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
|
||||
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_AUDIODATA), enable);
|
||||
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT), enable);
|
||||
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_WORKOUT), enable);
|
||||
if (characteristicChunked2021Read != null) {
|
||||
builder.notify(characteristicChunked2021Read, enable);
|
||||
}
|
||||
@ -1848,19 +1851,169 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
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;
|
||||
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()) {
|
||||
new TransactionBuilder("requestMtu")
|
||||
.requestMtu(mtu)
|
||||
.queue(getQueue());
|
||||
mMTU = mtu;
|
||||
/**
|
||||
* Track whether the currently selected workout needs gps (received in {@link #handleDeviceEvent}, so we can start the activity tracking
|
||||
* if needed in {@link #handleDeviceWorkoutEvent}, since in there we don't know what's the current workout.
|
||||
*/
|
||||
private boolean workoutNeedsGps = false;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
@ -1985,6 +2138,9 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
} else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) {
|
||||
handleDeviceEvent(characteristic.getValue());
|
||||
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)) {
|
||||
handleRealtimeSteps(characteristic.getValue());
|
||||
return true;
|
||||
@ -2026,6 +2182,9 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
} else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) {
|
||||
handleDeviceEvent(characteristic.getValue());
|
||||
return true;
|
||||
} else if (HuamiService.UUID_CHARACTERISTIC_WORKOUT.equals(characteristicUUID)) {
|
||||
handleDeviceWorkoutEvent(characteristic.getValue());
|
||||
return true;
|
||||
} else {
|
||||
LOG.info("Unhandled characteristic read: " + characteristicUUID);
|
||||
logMessageContent(characteristic.getValue());
|
||||
@ -3204,7 +3363,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
int pos = 2;
|
||||
|
||||
for (final String workoutType : enabledActivityTypes) {
|
||||
command[pos++] = HuamiWorkoutActivityType.fromPrefValue(workoutType).getCode();
|
||||
command[pos++] = HuamiWorkoutScreenActivityType.fromPrefValue(workoutType).getCode();
|
||||
command[pos++] = 0x00;
|
||||
command[pos++] = 0x01;
|
||||
}
|
||||
@ -3212,7 +3371,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
// Send all the remaining disabled workout types
|
||||
for (final String workoutType : allActivityTypes) {
|
||||
if (!enabledActivityTypes.contains(workoutType)) {
|
||||
command[pos++] = HuamiWorkoutActivityType.fromPrefValue(workoutType).getCode();
|
||||
command[pos++] = HuamiWorkoutScreenActivityType.fromPrefValue(workoutType).getCode();
|
||||
command[pos++] = 0x00;
|
||||
command[pos++] = 0x00;
|
||||
}
|
||||
|
@ -18,7 +18,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum HuamiWorkoutActivityType {
|
||||
/**
|
||||
* The workout types, to configure the workouts screen on the band.
|
||||
*/
|
||||
public enum HuamiWorkoutScreenActivityType {
|
||||
OutdoorRunning(0x01),
|
||||
Walking(0x06),
|
||||
Treadmill(0x08),
|
||||
@ -33,7 +36,7 @@ public enum HuamiWorkoutActivityType {
|
||||
|
||||
private final byte code;
|
||||
|
||||
HuamiWorkoutActivityType(final int code) {
|
||||
HuamiWorkoutScreenActivityType(final int code) {
|
||||
this.code = (byte) code;
|
||||
}
|
||||
|
||||
@ -41,12 +44,12 @@ public enum HuamiWorkoutActivityType {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static HuamiWorkoutActivityType fromPrefValue(final String prefValue) {
|
||||
for (HuamiWorkoutActivityType type : values()) {
|
||||
public static HuamiWorkoutScreenActivityType fromPrefValue(final String prefValue) {
|
||||
for (final HuamiWorkoutScreenActivityType type : values()) {
|
||||
if (type.name().toLowerCase(Locale.ROOT).equals(prefValue.replace("_", "").toLowerCase(Locale.ROOT))) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("No matching HuamiWorkoutActivityType for pref value: " + prefValue);
|
||||
throw new RuntimeException("No matching HuamiWorkoutScreenActivityType for pref value: " + prefValue);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.serial;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -289,4 +291,10 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport
|
||||
byte[] bytes = gbDeviceProtocol.encodeWorldClocks(clocks);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetGpsLocation(Location location) {
|
||||
byte[] bytes = gbDeviceProtocol.encodeGpsLocation(location);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.serial;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -163,4 +165,8 @@ public abstract class GBDeviceProtocol {
|
||||
public byte[] encodeFmFrequency(float frequency) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] encodeGpsLocation(Location location) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -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_ID_TRANSFER = "gadgetbridge transfer";
|
||||
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_INSTALL = 2;
|
||||
@ -72,6 +73,7 @@ public class GB {
|
||||
public static final int NOTIFICATION_ID_TRANSFER = 4;
|
||||
public static final int NOTIFICATION_ID_EXPORT_FAILED = 5;
|
||||
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;
|
||||
|
||||
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),
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
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;
|
||||
@ -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,
|
||||
int percentage, Context context) {
|
||||
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
|
||||
|
5
app/src/main/res/drawable/ic_gps_location.xml
Normal file
5
app/src/main/res/drawable/ic_gps_location.xml
Normal 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>
|
@ -256,6 +256,14 @@
|
||||
android:text="Show Fit.App.Track. Status"
|
||||
grid:layout_columnSpan="2"
|
||||
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>
|
||||
|
||||
</ScrollView>
|
||||
|
@ -370,6 +370,8 @@
|
||||
<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_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 -->
|
||||
<string name="pref_header_auto_export">Auto export</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_transfer_name">Data transfer</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_vergel">Amazfit Verge Lite</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_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_phone_gps_location_listener_stop">GPS Location Listener 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 -->
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user