mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-14 22:19:29 +01:00
Merge branch 'master' of codeberg.org:Freeyourgadget/Gadgetbridge
This commit is contained in:
commit
6fb22b9441
@ -1,7 +1,10 @@
|
||||
### Changelog
|
||||
|
||||
### NEXT
|
||||
* Amazfit Bip U/Pro/Band 5: enable extended HR/stress monitoring setting
|
||||
### 0.67.1
|
||||
* Huami: Fix long music track names not displaying
|
||||
* Amazfit Bip U/Pro/Band 5: Enable extended HR/stress monitoring setting
|
||||
* Pebble: Fix calendar blacklist, view and storage
|
||||
* FitPro: Fix crash, inactivity warning preference to string
|
||||
|
||||
### 0.67.0
|
||||
* Initial Support for Sony WF-1000XM3
|
||||
|
@ -55,8 +55,8 @@ android {
|
||||
multiDexEnabled true
|
||||
|
||||
// Note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.67.0"
|
||||
versionCode 211
|
||||
versionName "0.67.1"
|
||||
versionCode 212
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH_SHORT", "\"${getGitHashShort()}\""
|
||||
|
@ -595,6 +595,12 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activities.SleepAlarmWidgetConfigurationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ExternalPebbleJSActivity"
|
||||
android:allowTaskReparenting="true"
|
||||
@ -686,7 +692,7 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".externalevents.OpenTracksController"
|
||||
android:name=".externalevents.opentracks.OpenTracksController"
|
||||
android:label="OpenTracks controller and intent receiver"
|
||||
android:exported="true"/>
|
||||
</application>
|
||||
|
Binary file not shown.
@ -68,7 +68,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothStateChangeReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksContentObserver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
@ -117,7 +117,7 @@ public class GBApplication extends Application {
|
||||
private static SharedPreferences sharedPrefs;
|
||||
private static final String PREFS_VERSION = "shared_preferences_version";
|
||||
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
|
||||
private static final int CURRENT_PREFS_VERSION = 14;
|
||||
private static final int CURRENT_PREFS_VERSION = 15;
|
||||
|
||||
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
|
||||
private static Prefs prefs;
|
||||
@ -559,11 +559,11 @@ public class GBApplication extends Application {
|
||||
|
||||
private static HashSet<String> calendars_blacklist = null;
|
||||
|
||||
public static boolean calendarIsBlacklisted(String calendarDisplayName) {
|
||||
public static boolean calendarIsBlacklisted(String calendarUniqueName) {
|
||||
if (calendars_blacklist == null) {
|
||||
GB.log("calendarIsBlacklisted: calendars_blacklist is null!", GB.INFO, null);
|
||||
}
|
||||
return calendars_blacklist != null && calendars_blacklist.contains(calendarDisplayName);
|
||||
return calendars_blacklist != null && calendars_blacklist.contains(calendarUniqueName);
|
||||
}
|
||||
|
||||
public static void setCalendarsBlackList(Set<String> calendarNames) {
|
||||
@ -577,14 +577,18 @@ public class GBApplication extends Application {
|
||||
saveCalendarsBlackList();
|
||||
}
|
||||
|
||||
public static void addCalendarToBlacklist(String calendarDisplayName) {
|
||||
if (calendars_blacklist.add(calendarDisplayName)) {
|
||||
public static void addCalendarToBlacklist(String calendarUniqueName) {
|
||||
if (calendars_blacklist.add(calendarUniqueName)) {
|
||||
GB.log("Blacklisted calendar " + calendarUniqueName, GB.INFO, null);
|
||||
saveCalendarsBlackList();
|
||||
} else {
|
||||
GB.log("Calendar " + calendarUniqueName + " already blacklisted!", GB.WARN, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeFromCalendarBlacklist(String calendarDisplayName) {
|
||||
calendars_blacklist.remove(calendarDisplayName);
|
||||
public static void removeFromCalendarBlacklist(String calendarUniqueName) {
|
||||
calendars_blacklist.remove(calendarUniqueName);
|
||||
GB.log("Unblacklisted calendar " + calendarUniqueName, GB.INFO, null);
|
||||
saveCalendarsBlackList();
|
||||
}
|
||||
|
||||
@ -1143,6 +1147,25 @@ public class GBApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 15) {
|
||||
try (DBHandler db = acquireDB()) {
|
||||
final DaoSession daoSession = db.getDaoSession();
|
||||
final List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
|
||||
|
||||
for (Device dbDevice : activeDevices) {
|
||||
final SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
|
||||
final SharedPreferences.Editor deviceSharedPrefsEdit = deviceSharedPrefs.edit();
|
||||
|
||||
if (DeviceType.FITPRO.equals(dbDevice.getType())) {
|
||||
editor.remove("inactivity_warnings_threshold");
|
||||
deviceSharedPrefsEdit.apply();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "error acquiring DB lock");
|
||||
}
|
||||
}
|
||||
|
||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||
editor.apply();
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.Toast;
|
||||
|
||||
@ -37,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.WidgetPreferenceStorage;
|
||||
|
||||
/**
|
||||
* Implementation of SleepAlarmWidget functionality. When pressing the widget, an alarm will be set
|
||||
@ -44,11 +46,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
* value is retrieved using ActivityUser.().getSleepDuration().
|
||||
*/
|
||||
public class SleepAlarmWidget extends AppWidgetProvider {
|
||||
|
||||
/**
|
||||
* This is our dedicated action to detect when the widget has been clicked.
|
||||
*/
|
||||
public static final String ACTION =
|
||||
public static final String ACTION_CLICK =
|
||||
"nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK";
|
||||
|
||||
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
|
||||
@ -59,9 +60,10 @@ public class SleepAlarmWidget extends AppWidgetProvider {
|
||||
|
||||
// Add our own click intent
|
||||
Intent intent = new Intent(context, SleepAlarmWidget.class);
|
||||
intent.setAction(ACTION);
|
||||
intent.setAction(ACTION_CLICK);
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
PendingIntent clickPI = PendingIntent.getBroadcast(
|
||||
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
views.setOnClickPendingIntent(R.id.sleepalarmwidget_text, clickPI);
|
||||
|
||||
// Instruct the widget manager to update the widget
|
||||
@ -89,7 +91,15 @@ public class SleepAlarmWidget extends AppWidgetProvider {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
super.onReceive(context, intent);
|
||||
if (ACTION.equals(intent.getAction())) {
|
||||
Bundle extras = intent.getExtras();
|
||||
int appWidgetId = -1;
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
}
|
||||
|
||||
if (ACTION_CLICK.equals(intent.getAction())) {
|
||||
int userSleepDuration = new ActivityUser().getSleepDurationGoal();
|
||||
// current timestamp
|
||||
GregorianCalendar calendar = new GregorianCalendar();
|
||||
@ -102,16 +112,11 @@ public class SleepAlarmWidget extends AppWidgetProvider {
|
||||
|
||||
// overwrite the first alarm and activate it, without
|
||||
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext instanceof GBApplication) {
|
||||
GBApplication gbApp = (GBApplication) appContext;
|
||||
GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice();
|
||||
if (selectedDevice == null || !selectedDevice.isInitialized()) {
|
||||
GB.toast(context,
|
||||
context.getString(R.string.appwidget_not_connected),
|
||||
Toast.LENGTH_LONG, GB.WARN);
|
||||
return;
|
||||
}
|
||||
GBDevice deviceForWidget = new WidgetPreferenceStorage().getDeviceForWidget(appWidgetId);
|
||||
if (deviceForWidget == null || !deviceForWidget.isInitialized()) {
|
||||
GB.toast(context, context.getString(R.string.appwidget_not_connected),
|
||||
Toast.LENGTH_SHORT, GB.WARN);
|
||||
return;
|
||||
}
|
||||
|
||||
int hours = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
|
@ -54,11 +54,9 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.WidgetAlarmsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
@ -77,30 +75,9 @@ public class Widget extends AppWidgetProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Widget.class);
|
||||
static BroadcastReceiver broadcastReceiver = null;
|
||||
GBDevice selectedDevice;
|
||||
|
||||
private GBDevice getSelectedDevice() {
|
||||
Context context = GBApplication.getContext();
|
||||
if (!(context instanceof GBApplication)) {
|
||||
return null;
|
||||
}
|
||||
GBApplication gbApp = (GBApplication) context;
|
||||
return gbApp.getDeviceManager().getSelectedDevice();
|
||||
}
|
||||
|
||||
private GBDevice getDeviceByMAC(Context appContext, String HwAddress) {
|
||||
GBApplication gbApp = (GBApplication) appContext;
|
||||
List<? extends GBDevice> devices = gbApp.getDeviceManager().getDevices();
|
||||
for (GBDevice device : devices) {
|
||||
if (device.getAddress().equals(HwAddress)) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private long[] getSteps() {
|
||||
private long[] getSteps(GBDevice gbDevice) {
|
||||
Context context = GBApplication.getContext();
|
||||
Calendar day = GregorianCalendar.getInstance();
|
||||
|
||||
@ -108,7 +85,7 @@ public class Widget extends AppWidgetProvider {
|
||||
return new long[]{0, 0, 0};
|
||||
}
|
||||
DailyTotals ds = new DailyTotals();
|
||||
return ds.getDailyTotalsForDevice(selectedDevice, day);
|
||||
return ds.getDailyTotalsForDevice(gbDevice, day);
|
||||
//return ds.getDailyTotalsForAllDevices(day);
|
||||
}
|
||||
|
||||
@ -119,11 +96,10 @@ public class Widget extends AppWidgetProvider {
|
||||
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
|
||||
int appWidgetId) {
|
||||
|
||||
selectedDevice = getSelectedDevice();
|
||||
WidgetPreferenceStorage widgetPreferenceStorage = new WidgetPreferenceStorage();
|
||||
String savedDeviceAddress = widgetPreferenceStorage.getSavedDeviceAddress(context, appWidgetId);
|
||||
if (savedDeviceAddress != null) {
|
||||
selectedDevice = getDeviceByMAC(context.getApplicationContext(), savedDeviceAddress); //this would probably only happen if device no longer exists in GB
|
||||
GBDevice deviceForWidget = new WidgetPreferenceStorage().getDeviceForWidget(appWidgetId);
|
||||
if (deviceForWidget == null) {
|
||||
LOG.debug("Widget: no device, bailing out");
|
||||
return;
|
||||
}
|
||||
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
|
||||
@ -143,17 +119,17 @@ public class Widget extends AppWidgetProvider {
|
||||
|
||||
//alarms popup menu
|
||||
Intent startAlarmListIntent = new Intent(context, WidgetAlarmsActivity.class);
|
||||
startAlarmListIntent.putExtra(GBDevice.EXTRA_DEVICE, selectedDevice);
|
||||
startAlarmListIntent.putExtra(GBDevice.EXTRA_DEVICE, deviceForWidget);
|
||||
PendingIntent startAlarmListPIntent = PendingIntent.getActivity(context, appWidgetId, startAlarmListIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
views.setOnClickPendingIntent(R.id.todaywidget_header_alarm_icon, startAlarmListPIntent);
|
||||
|
||||
//charts
|
||||
Intent startChartsIntent = new Intent(context, ChartsActivity.class);
|
||||
startChartsIntent.putExtra(GBDevice.EXTRA_DEVICE, selectedDevice);
|
||||
startChartsIntent.putExtra(GBDevice.EXTRA_DEVICE, deviceForWidget);
|
||||
PendingIntent startChartsPIntent = PendingIntent.getActivity(context, appWidgetId, startChartsIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
views.setOnClickPendingIntent(R.id.todaywidget_bottom_layout, startChartsPIntent);
|
||||
|
||||
long[] dailyTotals = getSteps();
|
||||
long[] dailyTotals = getSteps(deviceForWidget);
|
||||
int steps = (int) dailyTotals[0];
|
||||
int sleep = (int) dailyTotals[1];
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
@ -165,6 +141,10 @@ public class Widget extends AppWidgetProvider {
|
||||
double distanceMeters = dailyTotals[0] * stepLength * 0.01;
|
||||
String distanceFormatted = FormatUtils.getFormattedDistanceLabel(distanceMeters);
|
||||
|
||||
if (sleep < 1) {
|
||||
views.setViewVisibility(R.id.todaywidget_sleep_layout, View.GONE);
|
||||
}
|
||||
|
||||
views.setTextViewText(R.id.todaywidget_steps, String.format("%1s", steps));
|
||||
views.setTextViewText(R.id.todaywidget_sleep, String.format("%1s", getHM(sleep)));
|
||||
views.setTextViewText(R.id.todaywidget_distance, distanceFormatted);
|
||||
@ -172,17 +152,17 @@ public class Widget extends AppWidgetProvider {
|
||||
views.setProgressBar(R.id.todaywidget_sleep_progress, sleepGoalMinutes, sleep, false);
|
||||
views.setProgressBar(R.id.todaywidget_distance_progress, distanceGoal, steps * stepLength, false);
|
||||
views.setViewVisibility(R.id.todaywidget_battery_icon, View.GONE);
|
||||
if (selectedDevice != null) {
|
||||
String status = String.format("%1s", selectedDevice.getStateString());
|
||||
if (selectedDevice.isConnected()) {
|
||||
if (selectedDevice.getBatteryLevel() > 1) {
|
||||
if (deviceForWidget != null) {
|
||||
String status = String.format("%1s", deviceForWidget.getStateString());
|
||||
if (deviceForWidget.isConnected()) {
|
||||
if (deviceForWidget.getBatteryLevel() > 1) {
|
||||
views.setViewVisibility(R.id.todaywidget_battery_icon, View.VISIBLE);
|
||||
|
||||
status = String.format("%1s%%", selectedDevice.getBatteryLevel());
|
||||
status = String.format("%1s%%", deviceForWidget.getBatteryLevel());
|
||||
}
|
||||
}
|
||||
|
||||
String deviceName = selectedDevice.getAlias() != null ? selectedDevice.getAlias() : selectedDevice.getName();
|
||||
String deviceName = deviceForWidget.getAlias() != null ? deviceForWidget.getAlias() : deviceForWidget.getName();
|
||||
views.setTextViewText(R.id.todaywidget_device_status, status);
|
||||
views.setTextViewText(R.id.todaywidget_device_name, deviceName);
|
||||
}
|
||||
@ -191,11 +171,11 @@ public class Widget extends AppWidgetProvider {
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
|
||||
public void refreshData() {
|
||||
public void refreshData(int appWidgetId) {
|
||||
Context context = GBApplication.getContext();
|
||||
GBDevice device = getSelectedDevice();
|
||||
GBDevice deviceForWidget = new WidgetPreferenceStorage().getDeviceForWidget(appWidgetId);
|
||||
|
||||
if (device == null || !device.isInitialized()) {
|
||||
if (deviceForWidget == null || !deviceForWidget.isInitialized()) {
|
||||
GB.toast(context,
|
||||
context.getString(R.string.device_not_connected),
|
||||
Toast.LENGTH_SHORT, GB.ERROR);
|
||||
@ -265,7 +245,7 @@ public class Widget extends AppWidgetProvider {
|
||||
super.onReceive(context, intent);
|
||||
LOG.debug("gbwidget LOCAL onReceive, action: " + intent.getAction() + intent);
|
||||
Bundle extras = intent.getExtras();
|
||||
int appWidgetId = -1;
|
||||
int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
@ -277,7 +257,7 @@ public class Widget extends AppWidgetProvider {
|
||||
if (broadcastReceiver == null) {
|
||||
onEnabled(context);
|
||||
}
|
||||
refreshData();
|
||||
refreshData(appWidgetId);
|
||||
//updateWidget();
|
||||
} else if (APPWIDGET_DELETED.equals(intent.getAction())) {
|
||||
onDisabled(context);
|
||||
|
@ -51,6 +51,7 @@ public class CalBlacklistActivity extends AbstractGBActivity {
|
||||
|
||||
private final String[] EVENT_PROJECTION = new String[]{
|
||||
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
|
||||
CalendarContract.Calendars.ACCOUNT_NAME,
|
||||
CalendarContract.Calendars.CALENDAR_COLOR
|
||||
};
|
||||
private ArrayList<Calendar> calendarsArrayList;
|
||||
@ -69,7 +70,7 @@ public class CalBlacklistActivity extends AbstractGBActivity {
|
||||
try (Cursor cur = getContentResolver().query(uri, EVENT_PROJECTION, null, null, null)) {
|
||||
calendarsArrayList = new ArrayList<>();
|
||||
while (cur != null && cur.moveToNext()) {
|
||||
calendarsArrayList.add(new Calendar(cur.getString(0), cur.getInt(1)));
|
||||
calendarsArrayList.add(new Calendar(cur.getString(0), cur.getString(1), cur.getInt(2)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,9 +83,9 @@ public class CalBlacklistActivity extends AbstractGBActivity {
|
||||
CheckBox selected = (CheckBox) view.findViewById(R.id.item_checkbox);
|
||||
toggleEntry(view);
|
||||
if (selected.isChecked()) {
|
||||
GBApplication.addCalendarToBlacklist(item.displayName);
|
||||
GBApplication.addCalendarToBlacklist(item.getUniqueString());
|
||||
} else {
|
||||
GBApplication.removeFromCalendarBlacklist(item.displayName);
|
||||
GBApplication.removeFromCalendarBlacklist(item.getUniqueString());
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -112,12 +113,18 @@ public class CalBlacklistActivity extends AbstractGBActivity {
|
||||
|
||||
class Calendar {
|
||||
private final String displayName;
|
||||
private final String accountName;
|
||||
private final int color;
|
||||
|
||||
public Calendar(String displayName, int color) {
|
||||
public Calendar(String displayName, String accountName, int color) {
|
||||
this.displayName = displayName;
|
||||
this.accountName = accountName;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public String getUniqueString() {
|
||||
return accountName + '/' + displayName;
|
||||
}
|
||||
}
|
||||
|
||||
private class CalendarListAdapter extends ArrayAdapter<Calendar> {
|
||||
@ -138,13 +145,16 @@ public class CalBlacklistActivity extends AbstractGBActivity {
|
||||
|
||||
View color = view.findViewById(R.id.calendar_color);
|
||||
TextView name = (TextView) view.findViewById(R.id.calendar_name);
|
||||
TextView ownerAccount = (TextView) view.findViewById(R.id.calendar_owner_account);
|
||||
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
|
||||
|
||||
if (GBApplication.calendarIsBlacklisted(item.displayName) && !checked.isChecked()) {
|
||||
if (GBApplication.calendarIsBlacklisted(item.getUniqueString()) && !checked.isChecked() ||
|
||||
!GBApplication.calendarIsBlacklisted(item.getUniqueString()) && checked.isChecked()) {
|
||||
toggleEntry(view);
|
||||
}
|
||||
color.setBackgroundColor(item.color);
|
||||
name.setText(item.displayName);
|
||||
ownerAccount.setText(item.accountName);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
@ -348,30 +348,30 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
case R.id.action_settings:
|
||||
Intent settingsIntent = new Intent(this, SettingsActivity.class);
|
||||
startActivityForResult(settingsIntent, MENU_REFRESH_CODE);
|
||||
return true;
|
||||
return false; //we do not want the drawer menu item to get selected
|
||||
case R.id.action_debug:
|
||||
Intent debugIntent = new Intent(this, DebugActivity.class);
|
||||
startActivity(debugIntent);
|
||||
return true;
|
||||
return false;
|
||||
case R.id.action_data_management:
|
||||
Intent dbIntent = new Intent(this, DataManagementActivity.class);
|
||||
startActivity(dbIntent);
|
||||
return true;
|
||||
return false;
|
||||
case R.id.action_notification_management:
|
||||
Intent blIntent = new Intent(this, NotificationManagementActivity.class);
|
||||
startActivity(blIntent);
|
||||
return true;
|
||||
return false;
|
||||
case R.id.device_action_discover:
|
||||
launchDiscoveryActivity();
|
||||
return true;
|
||||
return false;
|
||||
case R.id.action_quit:
|
||||
GBApplication.quit();
|
||||
return true;
|
||||
return false;
|
||||
case R.id.donation_link:
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
return true;
|
||||
return false;
|
||||
case R.id.external_changelog:
|
||||
ChangeLog cl = createChangeLog();
|
||||
try {
|
||||
@ -379,14 +379,14 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
} catch (Exception ignored) {
|
||||
GB.toast(getBaseContext(), "Error showing Changelog", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
case R.id.about:
|
||||
Intent aboutIntent = new Intent(this, AboutActivity.class);
|
||||
startActivity(aboutIntent);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private ChangeLog createChangeLog() {
|
||||
|
@ -71,8 +71,6 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
@ -86,8 +84,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksContentObserver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@ -529,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;
|
||||
|
||||
|
@ -0,0 +1,139 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
import android.widget.ListView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.WidgetPreferenceStorage;
|
||||
|
||||
public class SleepAlarmWidgetConfigurationActivity extends Activity {
|
||||
|
||||
// modified copy of WidgetConfigurationActivity
|
||||
// if we knew which widget is calling this config activity, we could only use a single configuration
|
||||
// activity and customize the filter in getAllDevices based on the caller.
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SleepAlarmWidgetConfigurationActivity.class);
|
||||
int mAppWidgetId;
|
||||
|
||||
LinkedHashMap<String, Pair<String, Integer>> allDevices;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
Intent intent = getIntent();
|
||||
Bundle extras = intent.getExtras();
|
||||
|
||||
if (extras != null) {
|
||||
mAppWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
}
|
||||
// make the result intent and set the result to canceled
|
||||
Intent resultValue;
|
||||
resultValue = new Intent();
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
|
||||
setResult(RESULT_CANCELED, resultValue);
|
||||
|
||||
if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish();
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(SleepAlarmWidgetConfigurationActivity.this);
|
||||
builder.setTitle(R.string.widget_settings_select_device_title);
|
||||
|
||||
allDevices = getAllDevices(getApplicationContext());
|
||||
|
||||
List<String> list = new ArrayList<>();
|
||||
for (Map.Entry<String, Pair<String, Integer>> item : allDevices.entrySet()) {
|
||||
list.add(item.getKey());
|
||||
}
|
||||
String[] allDevicesString = list.toArray(new String[0]);
|
||||
|
||||
builder.setSingleChoiceItems(allDevicesString, 0, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
}
|
||||
});
|
||||
|
||||
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
ListView lw = ((AlertDialog) dialog).getListView();
|
||||
int selectedItemPosition = lw.getCheckedItemPosition();
|
||||
|
||||
if (selectedItemPosition > -1) {
|
||||
Map.Entry<String, Pair<String, Integer>> selectedItem =
|
||||
(Map.Entry<String, Pair<String, Integer>>) allDevices.entrySet().toArray()[selectedItemPosition];
|
||||
WidgetPreferenceStorage widgetPreferenceStorage = new WidgetPreferenceStorage();
|
||||
widgetPreferenceStorage.saveWidgetPrefs(getApplicationContext(), String.valueOf(mAppWidgetId), selectedItem.getValue().first);
|
||||
}
|
||||
Intent resultValue = new Intent();
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
|
||||
setResult(RESULT_OK, resultValue);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent resultValue;
|
||||
resultValue = new Intent();
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
|
||||
setResult(RESULT_CANCELED, resultValue);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public LinkedHashMap getAllDevices(Context appContext) {
|
||||
DaoSession daoSession;
|
||||
GBApplication gbApp = (GBApplication) appContext;
|
||||
LinkedHashMap<String, Pair<String, Integer>> newMap = new LinkedHashMap<>(1);
|
||||
List<? extends GBDevice> devices = gbApp.getDeviceManager().getDevices();
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
daoSession = handler.getDaoSession();
|
||||
for (GBDevice device : devices) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
Device dbDevice = DBHelper.findDevice(device, daoSession);
|
||||
int icon = device.isInitialized() ? device.getType().getIcon() : device.getType().getDisabledIcon();
|
||||
if (dbDevice != null && coordinator != null
|
||||
&& (coordinator.getAlarmSlotCount() > 0)
|
||||
&& !newMap.containsKey(device.getAliasOrName())) {
|
||||
newMap.put(device.getAliasOrName(), new Pair(device.getAddress(), icon));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error getting list of all devices: " + e);
|
||||
}
|
||||
return newMap;
|
||||
}
|
||||
}
|
@ -40,17 +40,17 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
public class WidgetAlarmsActivity extends Activity implements View.OnClickListener {
|
||||
|
||||
TextView textView;
|
||||
GBDevice deviceForWidget;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Context appContext = this.getApplicationContext();
|
||||
GBDevice selectedDevice;
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
selectedDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
deviceForWidget = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
} else {
|
||||
GB.toast(this,
|
||||
"Error no device",
|
||||
@ -59,9 +59,8 @@ public class WidgetAlarmsActivity extends Activity implements View.OnClickListen
|
||||
}
|
||||
|
||||
if (appContext instanceof GBApplication) {
|
||||
GBApplication gbApp = (GBApplication) appContext;
|
||||
|
||||
if (selectedDevice == null || !selectedDevice.isInitialized()) {
|
||||
if (deviceForWidget == null || !deviceForWidget.isInitialized()) {
|
||||
GB.toast(this,
|
||||
this.getString(R.string.not_connected),
|
||||
Toast.LENGTH_LONG, GB.INFO);
|
||||
@ -128,16 +127,11 @@ public class WidgetAlarmsActivity extends Activity implements View.OnClickListen
|
||||
|
||||
// overwrite the first alarm and activate it, without
|
||||
|
||||
Context appContext = this.getApplicationContext();
|
||||
if (appContext instanceof GBApplication) {
|
||||
GBApplication gbApp = (GBApplication) appContext;
|
||||
GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice();
|
||||
if (selectedDevice == null || !selectedDevice.isInitialized()) {
|
||||
GB.toast(this,
|
||||
this.getString(R.string.appwidget_not_connected),
|
||||
Toast.LENGTH_LONG, GB.WARN);
|
||||
return;
|
||||
}
|
||||
if (deviceForWidget == null || !deviceForWidget.isInitialized()) {
|
||||
GB.toast(this,
|
||||
this.getString(R.string.appwidget_not_connected),
|
||||
Toast.LENGTH_LONG, GB.WARN);
|
||||
return;
|
||||
}
|
||||
|
||||
int hours = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
@ -152,6 +146,5 @@ public class WidgetAlarmsActivity extends Activity implements View.OnClickListen
|
||||
alarms.add(alarm);
|
||||
GBApplication.deviceService().onSetAlarms(alarms);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.WidgetPreferenceStorage;
|
||||
|
||||
public class WidgetConfigurationActivity extends Activity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Widget.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WidgetConfigurationActivity.class);
|
||||
int mAppWidgetId;
|
||||
|
||||
LinkedHashMap<String, Pair<String, Integer>> allDevices;
|
||||
@ -44,6 +44,7 @@ public class WidgetConfigurationActivity extends Activity {
|
||||
|
||||
Intent intent = getIntent();
|
||||
Bundle extras = intent.getExtras();
|
||||
|
||||
if (extras != null) {
|
||||
mAppWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
|
@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
@ -72,4 +73,17 @@ public class DeviceSettingsActivity extends AbstractGBActivity implements
|
||||
.commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// Simulate a back press, so that we don't actually exit the activity when
|
||||
// in a nested PreferenceScreen
|
||||
this.onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
@ -78,6 +78,7 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_INACTIVITY_START = "inactivity_warnings_start";
|
||||
public static final String PREF_INACTIVITY_END = "inactivity_warnings_end";
|
||||
public static final String PREF_INACTIVITY_THRESHOLD = "inactivity_warnings_threshold";
|
||||
public static final String PREF_INACTIVITY_THRESHOLD_EXTENDED = "inactivity_warnings_threshold_extended";
|
||||
public static final String PREF_INACTIVITY_MO = "inactivity_warnings_mo";
|
||||
public static final String PREF_INACTIVITY_TU = "inactivity_warnings_tu";
|
||||
public static final String PREF_INACTIVITY_WE = "inactivity_warnings_we";
|
||||
@ -116,6 +117,9 @@ 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_START_ON_PHONE = "workout_start_on_phone";
|
||||
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";
|
||||
|
@ -429,6 +429,7 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
addPreferenceHandlerFor(PREF_INACTIVITY_START);
|
||||
addPreferenceHandlerFor(PREF_INACTIVITY_END);
|
||||
addPreferenceHandlerFor(PREF_INACTIVITY_THRESHOLD);
|
||||
addPreferenceHandlerFor(PREF_INACTIVITY_THRESHOLD_EXTENDED);
|
||||
addPreferenceHandlerFor(PREF_INACTIVITY_MO);
|
||||
addPreferenceHandlerFor(PREF_INACTIVITY_TU);
|
||||
addPreferenceHandlerFor(PREF_INACTIVITY_WE);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -270,7 +270,7 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
public static int getHeartRateMeasurementInterval(String deviceAddress) {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return GBApplication.getPrefs().getInt(DeviceSettingsPreferenceConst.PREF_HEARTRATE_MEASUREMENT_INTERVAL, 0) / 60;
|
||||
return prefs.getInt(DeviceSettingsPreferenceConst.PREF_HEARTRATE_MEASUREMENT_INTERVAL, 0) / 60;
|
||||
}
|
||||
|
||||
public static boolean getHeartrateActivityMonitoring(String deviceAddress) throws IllegalArgumentException {
|
||||
@ -363,6 +363,18 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator {
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_LIFT_WRIST, false);
|
||||
}
|
||||
|
||||
public static boolean getWorkoutStartOnPhone(String deviceAddress) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
||||
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_START_ON_PHONE, 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");
|
||||
|
@ -110,6 +110,8 @@ public class AmazfitBand5Coordinator extends HuamiCoordinator {
|
||||
R.xml.devicesettings_nightmode,
|
||||
R.xml.devicesettings_liftwrist_display_sensitivity,
|
||||
R.xml.devicesettings_inactivity_dnd,
|
||||
R.xml.devicesettings_workout_start_on_phone,
|
||||
R.xml.devicesettings_workout_send_gps_to_band,
|
||||
R.xml.devicesettings_swipeunlock,
|
||||
R.xml.devicesettings_sync_calendar,
|
||||
R.xml.devicesettings_reserve_reminders_calendar,
|
||||
|
@ -115,6 +115,8 @@ public class MiBand5Coordinator extends HuamiCoordinator {
|
||||
R.xml.devicesettings_nightmode,
|
||||
R.xml.devicesettings_liftwrist_display_sensitivity,
|
||||
R.xml.devicesettings_inactivity_dnd,
|
||||
R.xml.devicesettings_workout_start_on_phone,
|
||||
R.xml.devicesettings_workout_send_gps_to_band,
|
||||
R.xml.devicesettings_swipeunlock,
|
||||
R.xml.devicesettings_sync_calendar,
|
||||
R.xml.devicesettings_reserve_reminders_calendar,
|
||||
|
@ -83,7 +83,7 @@ public class FossilFileReader {
|
||||
|
||||
short handle = buf.getShort();
|
||||
short version = buf.getShort();
|
||||
if ((handle == 5630) && (version == 3)) {
|
||||
if ((handle == 5630) && (version == 3 || version == 515 || version == 771)) {
|
||||
// This is a watch app or watch face
|
||||
isValid = true;
|
||||
isApp = true;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/* Copyright (C) 2022 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class OpenTracksContentObserver extends ContentObserver {
|
||||
private Context mContext;
|
||||
private Uri tracksUri;
|
||||
private int protocolVersion;
|
||||
private int totalTimeMillis;
|
||||
private float totalDistanceMeter;
|
||||
|
||||
private long previousTimeMillis = 0;
|
||||
private float previousDistanceMeter = 0;
|
||||
|
||||
public int getTotalTimeMillis() {
|
||||
return totalTimeMillis;
|
||||
}
|
||||
public float getTotalDistanceMeter() {
|
||||
return totalDistanceMeter;
|
||||
}
|
||||
|
||||
public long getTimeMillisChange() {
|
||||
/**
|
||||
* We don't use the timeMillis received from OpenTracks here, because those updates do not
|
||||
* come in very regularly when GPS reception is bad
|
||||
*/
|
||||
long timeMillisDelta = System.currentTimeMillis() - previousTimeMillis;
|
||||
previousTimeMillis = System.currentTimeMillis();
|
||||
return timeMillisDelta;
|
||||
}
|
||||
|
||||
public float getDistanceMeterChange() {
|
||||
float distanceMeterDelta = totalDistanceMeter - previousDistanceMeter;
|
||||
previousDistanceMeter = totalDistanceMeter;
|
||||
return distanceMeterDelta;
|
||||
}
|
||||
|
||||
|
||||
public OpenTracksContentObserver(Context context, final Uri tracksUri, final int protocolVersion) {
|
||||
super(new Handler());
|
||||
this.mContext = context;
|
||||
this.tracksUri = tracksUri;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.previousTimeMillis = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(final boolean selfChange, final Uri uri) {
|
||||
if (uri == null) {
|
||||
return; // nothing can be done without an uri
|
||||
}
|
||||
if (tracksUri.toString().startsWith(uri.toString())) {
|
||||
final List<Track> tracks = Track.readTracks(mContext.getContentResolver(), tracksUri, protocolVersion);
|
||||
if (!tracks.isEmpty()) {
|
||||
final TrackStatistics statistics = new TrackStatistics(tracks);
|
||||
totalTimeMillis = statistics.getTotalTimeMillis();
|
||||
totalDistanceMeter = statistics.getTotalDistanceMeter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
if (mContext != null) {
|
||||
mContext.getContentResolver().unregisterContentObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
unregister();
|
||||
if (mContext != null) {
|
||||
((Activity) mContext).finish();
|
||||
mContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
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;
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
@ -93,7 +93,7 @@ public class OpenTracksController extends Activity {
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setClassName(packageName, className);
|
||||
intent.putExtra("STATS_TARGET_PACKAGE", context.getPackageName());
|
||||
intent.putExtra("STATS_TARGET_CLASS", "nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController");
|
||||
intent.putExtra("STATS_TARGET_CLASS", OpenTracksController.class.getName());
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (Exception e) {
|
@ -15,15 +15,11 @@
|
||||
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;
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -31,84 +27,11 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class OpenTracksContentObserver extends ContentObserver {
|
||||
private Context mContext;
|
||||
private Uri tracksUri;
|
||||
private int protocolVersion;
|
||||
private int totalTimeMillis;
|
||||
private float totalDistanceMeter;
|
||||
|
||||
private long previousTimeMillis = 0;
|
||||
private float previousDistanceMeter = 0;
|
||||
|
||||
public int getTotalTimeMillis() {
|
||||
return totalTimeMillis;
|
||||
}
|
||||
public float getTotalDistanceMeter() {
|
||||
return totalDistanceMeter;
|
||||
}
|
||||
|
||||
public long getTimeMillisChange() {
|
||||
/**
|
||||
* We don't use the timeMillis received from OpenTracks here, because those updates do not
|
||||
* come in very regularly when GPS reception is bad
|
||||
*/
|
||||
long timeMillisDelta = System.currentTimeMillis() - previousTimeMillis;
|
||||
previousTimeMillis = System.currentTimeMillis();
|
||||
return timeMillisDelta;
|
||||
}
|
||||
|
||||
public float getDistanceMeterChange() {
|
||||
float distanceMeterDelta = totalDistanceMeter - previousDistanceMeter;
|
||||
previousDistanceMeter = totalDistanceMeter;
|
||||
return distanceMeterDelta;
|
||||
}
|
||||
|
||||
|
||||
public OpenTracksContentObserver(Context context, final Uri tracksUri, final int protocolVersion) {
|
||||
super(new Handler());
|
||||
this.mContext = context;
|
||||
this.tracksUri = tracksUri;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.previousTimeMillis = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(final boolean selfChange, final Uri uri) {
|
||||
if (uri == null) {
|
||||
return; // nothing can be done without an uri
|
||||
}
|
||||
if (tracksUri.toString().startsWith(uri.toString())) {
|
||||
final List<Track> tracks = Track.readTracks(mContext.getContentResolver(), tracksUri, protocolVersion);
|
||||
if (!tracks.isEmpty()) {
|
||||
final TrackStatistics statistics = new TrackStatistics(tracks);
|
||||
totalTimeMillis = statistics.getTotalTimeMillis();
|
||||
totalDistanceMeter = statistics.getTotalDistanceMeter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
if (mContext != null) {
|
||||
mContext.getContentResolver().unregisterContentObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
unregister();
|
||||
if (mContext != null) {
|
||||
((Activity) mContext).finish();
|
||||
mContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class was copied and modified from
|
||||
* https://github.com/OpenTracksApp/OSMDashboard/blob/main/src/main/java/de/storchp/opentracks/osmplugin/dashboardapi/Track.java
|
||||
*/
|
||||
class Track {
|
||||
/**
|
||||
* This class was copied and modified from
|
||||
* https://github.com/OpenTracksApp/OSMDashboard/blob/main/src/main/java/de/storchp/opentracks/osmplugin/dashboardapi/Track.java
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Track.class);
|
||||
|
||||
private static final String TAG = Track.class.getSimpleName();
|
||||
@ -280,114 +203,3 @@ class Track {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
class TrackStatistics {
|
||||
/**
|
||||
* This class was copied and modified from
|
||||
* https://github.com/OpenTracksApp/OSMDashboard/blob/main/src/main/java/de/storchp/opentracks/osmplugin/utils/TrackStatistics.java
|
||||
*/
|
||||
|
||||
private String category = "unknown";
|
||||
private int startTimeEpochMillis;
|
||||
private int stopTimeEpochMillis;
|
||||
private float totalDistanceMeter;
|
||||
private int totalTimeMillis;
|
||||
private int movingTimeMillis;
|
||||
private float avgSpeedMeterPerSecond;
|
||||
private float avgMovingSpeedMeterPerSecond;
|
||||
private float maxSpeedMeterPerSecond;
|
||||
private float minElevationMeter;
|
||||
private float maxElevationMeter;
|
||||
private float elevationGainMeter;
|
||||
|
||||
public TrackStatistics(final List<Track> tracks) {
|
||||
if (tracks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final Track first = tracks.get(0);
|
||||
category = first.getCategory();
|
||||
startTimeEpochMillis = first.getStartTimeEpochMillis();
|
||||
stopTimeEpochMillis = first.getStopTimeEpochMillis();
|
||||
totalDistanceMeter = first.getTotalDistanceMeter();
|
||||
totalTimeMillis = first.getTotalTimeMillis();
|
||||
movingTimeMillis = first.getMovingTimeMillis();
|
||||
avgSpeedMeterPerSecond = first.getAvgSpeedMeterPerSecond();
|
||||
avgMovingSpeedMeterPerSecond = first.getAvgMovingSpeedMeterPerSecond();
|
||||
maxSpeedMeterPerSecond = first.getMaxSpeedMeterPerSecond();
|
||||
minElevationMeter = first.getMinElevationMeter();
|
||||
maxElevationMeter = first.getMaxElevationMeter();
|
||||
elevationGainMeter = first.getElevationGainMeter();
|
||||
|
||||
if (tracks.size() > 1) {
|
||||
float totalAvgSpeedMeterPerSecond = avgSpeedMeterPerSecond;
|
||||
float totalAvgMovingSpeedMeterPerSecond = avgMovingSpeedMeterPerSecond;
|
||||
for (final Track track : tracks.subList(1, tracks.size())) {
|
||||
if (!category.equals(track.getCategory())) {
|
||||
category = "mixed";
|
||||
}
|
||||
startTimeEpochMillis = Math.min(startTimeEpochMillis, track.getStartTimeEpochMillis());
|
||||
stopTimeEpochMillis = Math.max(stopTimeEpochMillis, track.getStopTimeEpochMillis());
|
||||
totalDistanceMeter += track.getTotalDistanceMeter();
|
||||
totalTimeMillis += track.getTotalTimeMillis();
|
||||
movingTimeMillis += track.getMovingTimeMillis();
|
||||
totalAvgSpeedMeterPerSecond += track.getAvgSpeedMeterPerSecond();
|
||||
totalAvgMovingSpeedMeterPerSecond += track.getAvgMovingSpeedMeterPerSecond();
|
||||
maxSpeedMeterPerSecond = Math.max(maxSpeedMeterPerSecond, track.getMaxSpeedMeterPerSecond());
|
||||
minElevationMeter = Math.min(minElevationMeter, track.getMinElevationMeter());
|
||||
maxElevationMeter = Math.max(maxElevationMeter, track.getMaxElevationMeter());
|
||||
elevationGainMeter += track.getElevationGainMeter();
|
||||
}
|
||||
|
||||
avgSpeedMeterPerSecond = totalAvgSpeedMeterPerSecond / tracks.size();
|
||||
avgMovingSpeedMeterPerSecond = totalAvgMovingSpeedMeterPerSecond / tracks.size();
|
||||
}
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public int getStartTimeEpochMillis() {
|
||||
return startTimeEpochMillis;
|
||||
}
|
||||
|
||||
public int getStopTimeEpochMillis() {
|
||||
return stopTimeEpochMillis;
|
||||
}
|
||||
|
||||
public float getTotalDistanceMeter() {
|
||||
return totalDistanceMeter;
|
||||
}
|
||||
|
||||
public int getTotalTimeMillis() {
|
||||
return totalTimeMillis;
|
||||
}
|
||||
|
||||
public int getMovingTimeMillis() {
|
||||
return movingTimeMillis;
|
||||
}
|
||||
|
||||
public float getAvgSpeedMeterPerSecond() {
|
||||
return avgSpeedMeterPerSecond;
|
||||
}
|
||||
|
||||
public float getAvgMovingSpeedMeterPerSecond() {
|
||||
return avgMovingSpeedMeterPerSecond;
|
||||
}
|
||||
|
||||
public float getMaxSpeedMeterPerSecond() {
|
||||
return maxSpeedMeterPerSecond;
|
||||
}
|
||||
|
||||
public float getMinElevationMeter() {
|
||||
return minElevationMeter;
|
||||
}
|
||||
|
||||
public float getMaxElevationMeter() {
|
||||
return maxElevationMeter;
|
||||
}
|
||||
|
||||
public float getElevationGainMeter() {
|
||||
return elevationGainMeter;
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/* Copyright (C) 2022 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class was copied and modified from
|
||||
* https://github.com/OpenTracksApp/OSMDashboard/blob/main/src/main/java/de/storchp/opentracks/osmplugin/utils/TrackStatistics.java
|
||||
*/
|
||||
class TrackStatistics {
|
||||
private String category = "unknown";
|
||||
private int startTimeEpochMillis;
|
||||
private int stopTimeEpochMillis;
|
||||
private float totalDistanceMeter;
|
||||
private int totalTimeMillis;
|
||||
private int movingTimeMillis;
|
||||
private float avgSpeedMeterPerSecond;
|
||||
private float avgMovingSpeedMeterPerSecond;
|
||||
private float maxSpeedMeterPerSecond;
|
||||
private float minElevationMeter;
|
||||
private float maxElevationMeter;
|
||||
private float elevationGainMeter;
|
||||
|
||||
public TrackStatistics(final List<Track> tracks) {
|
||||
if (tracks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final Track first = tracks.get(0);
|
||||
category = first.getCategory();
|
||||
startTimeEpochMillis = first.getStartTimeEpochMillis();
|
||||
stopTimeEpochMillis = first.getStopTimeEpochMillis();
|
||||
totalDistanceMeter = first.getTotalDistanceMeter();
|
||||
totalTimeMillis = first.getTotalTimeMillis();
|
||||
movingTimeMillis = first.getMovingTimeMillis();
|
||||
avgSpeedMeterPerSecond = first.getAvgSpeedMeterPerSecond();
|
||||
avgMovingSpeedMeterPerSecond = first.getAvgMovingSpeedMeterPerSecond();
|
||||
maxSpeedMeterPerSecond = first.getMaxSpeedMeterPerSecond();
|
||||
minElevationMeter = first.getMinElevationMeter();
|
||||
maxElevationMeter = first.getMaxElevationMeter();
|
||||
elevationGainMeter = first.getElevationGainMeter();
|
||||
|
||||
if (tracks.size() > 1) {
|
||||
float totalAvgSpeedMeterPerSecond = avgSpeedMeterPerSecond;
|
||||
float totalAvgMovingSpeedMeterPerSecond = avgMovingSpeedMeterPerSecond;
|
||||
for (final Track track : tracks.subList(1, tracks.size())) {
|
||||
if (!category.equals(track.getCategory())) {
|
||||
category = "mixed";
|
||||
}
|
||||
startTimeEpochMillis = Math.min(startTimeEpochMillis, track.getStartTimeEpochMillis());
|
||||
stopTimeEpochMillis = Math.max(stopTimeEpochMillis, track.getStopTimeEpochMillis());
|
||||
totalDistanceMeter += track.getTotalDistanceMeter();
|
||||
totalTimeMillis += track.getTotalTimeMillis();
|
||||
movingTimeMillis += track.getMovingTimeMillis();
|
||||
totalAvgSpeedMeterPerSecond += track.getAvgSpeedMeterPerSecond();
|
||||
totalAvgMovingSpeedMeterPerSecond += track.getAvgMovingSpeedMeterPerSecond();
|
||||
maxSpeedMeterPerSecond = Math.max(maxSpeedMeterPerSecond, track.getMaxSpeedMeterPerSecond());
|
||||
minElevationMeter = Math.min(minElevationMeter, track.getMinElevationMeter());
|
||||
maxElevationMeter = Math.max(maxElevationMeter, track.getMaxElevationMeter());
|
||||
elevationGainMeter += track.getElevationGainMeter();
|
||||
}
|
||||
|
||||
avgSpeedMeterPerSecond = totalAvgSpeedMeterPerSecond / tracks.size();
|
||||
avgMovingSpeedMeterPerSecond = totalAvgMovingSpeedMeterPerSecond / tracks.size();
|
||||
}
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public int getStartTimeEpochMillis() {
|
||||
return startTimeEpochMillis;
|
||||
}
|
||||
|
||||
public int getStopTimeEpochMillis() {
|
||||
return stopTimeEpochMillis;
|
||||
}
|
||||
|
||||
public float getTotalDistanceMeter() {
|
||||
return totalDistanceMeter;
|
||||
}
|
||||
|
||||
public int getTotalTimeMillis() {
|
||||
return totalTimeMillis;
|
||||
}
|
||||
|
||||
public int getMovingTimeMillis() {
|
||||
return movingTimeMillis;
|
||||
}
|
||||
|
||||
public float getAvgSpeedMeterPerSecond() {
|
||||
return avgSpeedMeterPerSecond;
|
||||
}
|
||||
|
||||
public float getAvgMovingSpeedMeterPerSecond() {
|
||||
return avgMovingSpeedMeterPerSecond;
|
||||
}
|
||||
|
||||
public float getMaxSpeedMeterPerSecond() {
|
||||
return maxSpeedMeterPerSecond;
|
||||
}
|
||||
|
||||
public float getMinElevationMeter() {
|
||||
return minElevationMeter;
|
||||
}
|
||||
|
||||
public float getMaxElevationMeter() {
|
||||
return maxElevationMeter;
|
||||
}
|
||||
|
||||
public float getElevationGainMeter() {
|
||||
return elevationGainMeter;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Instances;
|
||||
import android.text.format.Time;
|
||||
|
||||
@ -56,6 +57,8 @@ public class CalendarEvents {
|
||||
Instances.DESCRIPTION,
|
||||
Instances.EVENT_LOCATION,
|
||||
Instances.CALENDAR_DISPLAY_NAME,
|
||||
CalendarContract.Calendars.ACCOUNT_NAME,
|
||||
Instances.CALENDAR_COLOR,
|
||||
Instances.ALL_DAY
|
||||
};
|
||||
|
||||
@ -101,12 +104,14 @@ public class CalendarEvents {
|
||||
evtCursor.getString(5),
|
||||
evtCursor.getString(6),
|
||||
evtCursor.getString(7),
|
||||
!evtCursor.getString(8).equals("0")
|
||||
evtCursor.getString(8),
|
||||
evtCursor.getInt(9),
|
||||
!evtCursor.getString(10).equals("0")
|
||||
);
|
||||
if (!GBApplication.calendarIsBlacklisted(calEvent.getCalName())) {
|
||||
if (!GBApplication.calendarIsBlacklisted(calEvent.getUniqueCalName())) {
|
||||
calendarEventList.add(calEvent);
|
||||
} else {
|
||||
LOG.debug("calendar " + calEvent.getCalName() + " skipped because it's blacklisted");
|
||||
LOG.debug("calendar " + calEvent.getUniqueCalName() + " skipped because it's blacklisted");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@ -124,9 +129,11 @@ public class CalendarEvents {
|
||||
private String description;
|
||||
private String location;
|
||||
private String calName;
|
||||
private String calAccountName;
|
||||
private int color;
|
||||
private boolean allDay;
|
||||
|
||||
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, boolean allDay) {
|
||||
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, String calAccountName, int color, boolean allDay) {
|
||||
this.begin = begin;
|
||||
this.end = end;
|
||||
this.id = id;
|
||||
@ -134,6 +141,8 @@ public class CalendarEvents {
|
||||
this.description = description;
|
||||
this.location = location;
|
||||
this.calName = calName;
|
||||
this.calAccountName = calAccountName;
|
||||
this.color = color;
|
||||
this.allDay = allDay;
|
||||
}
|
||||
|
||||
@ -182,6 +191,18 @@ public class CalendarEvents {
|
||||
return calName;
|
||||
}
|
||||
|
||||
public String getCalAccountName() {
|
||||
return calAccountName;
|
||||
}
|
||||
|
||||
public String getUniqueCalName() {
|
||||
return getCalAccountName() + '/' + getCalName();
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public boolean isAllDay() {
|
||||
return allDay;
|
||||
}
|
||||
@ -197,6 +218,8 @@ public class CalendarEvents {
|
||||
Objects.equals(this.getDescription(), e.getDescription()) &&
|
||||
(this.getEnd() == e.getEnd()) &&
|
||||
Objects.equals(this.getCalName(), e.getCalName()) &&
|
||||
Objects.equals(this.getCalAccountName(), e.getCalAccountName()) &&
|
||||
(this.getColor() == e.getColor()) &&
|
||||
(this.isAllDay() == e.isAllDay());
|
||||
} else {
|
||||
return false;
|
||||
@ -212,6 +235,8 @@ public class CalendarEvents {
|
||||
result = 31 * result + Objects.hash(description);
|
||||
result = 31 * result + Long.valueOf(end).hashCode();
|
||||
result = 31 * result + Objects.hash(calName);
|
||||
result = 31 * result + Objects.hash(calAccountName);
|
||||
result = 31 * result + Integer.valueOf(color).hashCode();
|
||||
result = 31 * result + Boolean.valueOf(allDay).hashCode();
|
||||
return result;
|
||||
}
|
||||
|
@ -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;
|
||||
@ -119,6 +120,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE
|
||||
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_GPS_LOCATION;
|
||||
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;
|
||||
@ -149,6 +151,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CON
|
||||
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_GPS_LOCATION;
|
||||
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;
|
||||
@ -657,6 +660,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) {
|
||||
|
||||
|
@ -345,10 +345,17 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
/// Write JSON object of the form {t:taskName, err:message}
|
||||
|
||||
private void uartTxJSONError(String taskName, String message) {
|
||||
uartTxJSONError(taskName,message,null);
|
||||
}
|
||||
|
||||
private void uartTxJSONError(String taskName, String message,String id) {
|
||||
JSONObject o = new JSONObject();
|
||||
try {
|
||||
o.put("t", taskName);
|
||||
if( id!=null)
|
||||
o.put("id", id);
|
||||
o.put("err", message);
|
||||
} catch (JSONException e) {
|
||||
GB.toast(getContext(), "uartTxJSONError: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
@ -356,6 +363,8 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
uartTxJSON(taskName, o);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void handleUartRxLine(String line) {
|
||||
LOG.info("UART RX LINE: " + line);
|
||||
if (line.length()==0) return;
|
||||
@ -481,9 +490,18 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
} break;
|
||||
case "http": {
|
||||
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
String _id=null;
|
||||
try {
|
||||
_id = json.getString("id");
|
||||
} catch (JSONException e) {
|
||||
}
|
||||
final String id = _id;
|
||||
|
||||
|
||||
if (BuildConfig.INTERNET_ACCESS && devicePrefs.getBoolean(PREF_DEVICE_INTERNET_ACCESS, false)) {
|
||||
RequestQueue queue = Volley.newRequestQueue(getContext());
|
||||
String url = json.getString("url");
|
||||
|
||||
String _xmlPath = "";
|
||||
try {
|
||||
_xmlPath = json.getString("xpath");
|
||||
@ -502,12 +520,14 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
response = xPath.evaluate(xmlPath, inputXML);
|
||||
} catch (Exception error) {
|
||||
uartTxJSONError("http", error.toString());
|
||||
uartTxJSONError("http", error.toString(),id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
o.put("t", "http");
|
||||
if( id!=null)
|
||||
o.put("id", id);
|
||||
o.put("resp", response);
|
||||
} catch (JSONException e) {
|
||||
GB.toast(getContext(), "HTTP: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
@ -518,15 +538,15 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
JSONObject o = new JSONObject();
|
||||
uartTxJSONError("http", error.toString());
|
||||
uartTxJSONError("http", error.toString(),id);
|
||||
}
|
||||
});
|
||||
queue.add(stringRequest);
|
||||
} else {
|
||||
if (BuildConfig.INTERNET_ACCESS)
|
||||
uartTxJSONError("http", "Internet access not enabled, check Gadgetbridge Device Settings");
|
||||
uartTxJSONError("http", "Internet access not enabled, check Gadgetbridge Device Settings",id);
|
||||
else
|
||||
uartTxJSONError("http", "Internet access not enabled in this Gadgetbridge build");
|
||||
uartTxJSONError("http", "Internet access not enabled in this Gadgetbridge build",id);
|
||||
}
|
||||
} break;
|
||||
case "intent": {
|
||||
|
@ -543,7 +543,7 @@ public class FitProDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
case DeviceSettingsPreferenceConst.PREF_LANGUAGE:
|
||||
setLanguage(builder);
|
||||
break;
|
||||
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD:
|
||||
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD_EXTENDED:
|
||||
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE:
|
||||
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_START:
|
||||
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_END:
|
||||
@ -1148,7 +1148,7 @@ public class FitProDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
if (prefLongsitSwitch) {
|
||||
|
||||
String inactivity = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD, "4");
|
||||
String inactivity = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD_EXTENDED, "4");
|
||||
String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_START, "08:00");
|
||||
String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_END, "16:00");
|
||||
Calendar startCalendar = GregorianCalendar.getInstance();
|
||||
|
@ -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,7 +103,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
@ -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,181 @@ 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());
|
||||
final boolean startOnPhone = HuamiCoordinator.getWorkoutStartOnPhone(getDevice().getAddress());
|
||||
|
||||
switch (status) {
|
||||
case Start:
|
||||
if (workoutNeedsGps && startOnPhone) {
|
||||
LOG.info("Starting OpenTracks recording");
|
||||
|
||||
OpenTracksController.startRecording(getContext());
|
||||
}
|
||||
|
||||
break;
|
||||
case End:
|
||||
GBLocationManager.stop(getContext(), this);
|
||||
|
||||
if (startOnPhone) {
|
||||
LOG.info("Stopping OpenTracks recording");
|
||||
OpenTracksController.stopRecording(getContext());
|
||||
}
|
||||
|
||||
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 +2150,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 +2194,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 +3375,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 +3383,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;
|
||||
}
|
||||
}
|
@ -93,10 +93,10 @@ public class MiBand3Support extends AmazfitBipSupport {
|
||||
|
||||
switch (nightMode) {
|
||||
case MiBandConst.PREF_NIGHT_MODE_SUNSET:
|
||||
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand3Service.COMMAND_NIGHT_MODE_SUNSET);
|
||||
writeToConfiguration(builder, MiBand3Service.COMMAND_NIGHT_MODE_SUNSET);
|
||||
break;
|
||||
case MiBandConst.PREF_NIGHT_MODE_OFF:
|
||||
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand3Service.COMMAND_NIGHT_MODE_OFF);
|
||||
writeToConfiguration(builder, MiBand3Service.COMMAND_NIGHT_MODE_OFF);
|
||||
break;
|
||||
case MiBandConst.PREF_NIGHT_MODE_SCHEDULED:
|
||||
byte[] cmd = MiBand3Service.COMMAND_NIGHT_MODE_SCHEDULED.clone();
|
||||
@ -113,7 +113,7 @@ public class MiBand3Support extends AmazfitBipSupport {
|
||||
cmd[4] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
|
||||
cmd[5] = (byte) calendar.get(Calendar.MINUTE);
|
||||
|
||||
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), cmd);
|
||||
writeToConfiguration(builder, cmd);
|
||||
break;
|
||||
default:
|
||||
LOG.error("Invalid night mode: " + nightMode);
|
||||
|
@ -39,11 +39,12 @@ public class MiBand6FirmwareInfo extends HuamiFirmwareInfo {
|
||||
// firmware
|
||||
crcToVersion.put(47447, "1.0.1.36");
|
||||
crcToVersion.put(41380, "1.0.4.38");
|
||||
crcToVersion.put(8209, "1.0.6.10");
|
||||
crcToVersion.put(8209, "1.0.6.10-16");
|
||||
// resources
|
||||
crcToVersion.put(54803, "1.0.1.36");
|
||||
crcToVersion.put(14596, "1.0.4.38");
|
||||
crcToVersion.put(63397, "1.0.6.10");
|
||||
crcToVersion.put(19391, "1.0.6.16");
|
||||
}
|
||||
|
||||
public MiBand6FirmwareInfo(byte[] bytes) {
|
||||
|
@ -943,6 +943,10 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
|
||||
@Override
|
||||
public void setMusicInfo(MusicSpec musicSpec) {
|
||||
musicSpec = new MusicSpec(musicSpec);
|
||||
if(musicSpec.album == null) musicSpec.album = "";
|
||||
if(musicSpec.artist == null) musicSpec.artist = "";
|
||||
if(musicSpec.track == null) musicSpec.track = "";
|
||||
if (
|
||||
currentSpec != null
|
||||
&& currentSpec.album.equals(musicSpec.album)
|
||||
|
@ -25,7 +25,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
|
||||
|
||||
public class WorkoutRequestHandler {
|
||||
public static void addStateResponse(JSONObject workoutResponse, String type, String msg) throws JSONException {
|
||||
|
@ -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);
|
||||
|
@ -26,7 +26,11 @@ import org.json.JSONException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.Widget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class WidgetPreferenceStorage {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WidgetPreferenceStorage.class);
|
||||
@ -151,4 +155,31 @@ public class WidgetPreferenceStorage {
|
||||
}
|
||||
GB.toast("Saved app widget preferences: " + savedWidgetsPreferencesDataArray, Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
|
||||
public GBDevice getDeviceForWidget(int appWidgetId) {
|
||||
Context context = GBApplication.getContext();
|
||||
if (!(context instanceof GBApplication)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String savedDeviceAddress = getSavedDeviceAddress(context, appWidgetId);
|
||||
|
||||
if (savedDeviceAddress != null) {
|
||||
return getDeviceByMAC(context.getApplicationContext(), savedDeviceAddress); //this would probably only happen if device no longer exists in GB
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private GBDevice getDeviceByMAC(Context appContext, String HwAddress) {
|
||||
GBApplication gbApp = (GBApplication) appContext;
|
||||
List<? extends GBDevice> devices = gbApp.getDeviceManager().getDevices();
|
||||
for (GBDevice device : devices) {
|
||||
if (device.getAddress().equals(HwAddress)) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
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>
|
||||
|
@ -25,8 +25,8 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_toEndOf="@+id/item_checkbox"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingTop="8dp" />
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
@ -36,6 +36,16 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/calendar_owner_account"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollHorizontally="false"
|
||||
android:maxLines="1"
|
||||
android:text="TextView"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/calendar_name"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
|
@ -171,7 +171,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:id="@+id/todaywidget_sleep_layout">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/todaywidget_sleep_icon"
|
||||
|
@ -1023,7 +1023,7 @@
|
||||
<string name="km">km</string>
|
||||
<string name="seconds_m">sec/m</string>
|
||||
<string name="activity_type_yoga">Jóga</string>
|
||||
<string name="activity_type_jump_roping">Skákací lano</string>
|
||||
<string name="activity_type_jump_roping">Švihadlo</string>
|
||||
<string name="activity_type_elliptical_trainer">Eliptický trenažér</string>
|
||||
<string name="activity_type_indoor_cycling">Sálová Cyklistika</string>
|
||||
<string name="activity_type_swimming_openwater">Plavání (otevřená voda)</string>
|
||||
@ -1641,4 +1641,77 @@
|
||||
<string name="pref_world_clocks_summary">Konfigurace hodin pro jiná časová pásma</string>
|
||||
<string name="world_clock_delete_confirm_title">Odstranit \'%1$s\'</string>
|
||||
<string name="world_clock_no_free_slots_title">Žádné volné místo</string>
|
||||
<string name="devicetype_sony_wf_1000xm3">Sony WF-1000XM3</string>
|
||||
<string name="devicetype_galaxybuds_pro">Galaxy Buds Pro</string>
|
||||
<string name="pref_title_touch_ambient">Okolní zvuk</string>
|
||||
<string name="pref_title_device_internet_access">Povolit přístup k internetu</string>
|
||||
<string name="pref_summary_device_internet_access">Povolit všem aplikacím na hodinkách přístup k internetu</string>
|
||||
<string name="pref_title_device_intents">Povolit Intenty</string>
|
||||
<string name="heartrate_bpm_105">105 tepů/min</string>
|
||||
<string name="heartrate_bpm_100">100 tepů/min</string>
|
||||
<string name="heartrate_bpm_110">110 tepů/min</string>
|
||||
<string name="heartrate_bpm_112">112 tepů/min</string>
|
||||
<string name="prefs_switch_control_right">Ovládání přepínání Vpravo</string>
|
||||
<string name="pref_title_touch_volume">Hlasitost</string>
|
||||
<string name="pref_title_touch_spotify">Spotify</string>
|
||||
<string name="pref_switch_noise_control">Přepínání regulace hluku</string>
|
||||
<string name="permission_notification_listener">%1$s potřebuje přístup k Notifikacím, aby bylo možno notifikace zobrazovat i na připojených hodinkách/náramku.
|
||||
\n
|
||||
\nZvolte \'%2$s\', poté \'%1$s\' a a vyberte \'Povolit přístup k Notifikacím\', poté zvolte \'Zpět\' pro návrat do %1$s</string>
|
||||
<string name="permission_notification_policy_access">%1$s potřebuje přístup k funkci Nerušit, aby bylo možno nastavit Nerušit i na připojených hodinkách/náramku.
|
||||
\n
|
||||
\nZvolte \'%2$s\', poté \'%1$s\' a a vyberte \'Povolit přístup funkci Nerušit\', poté zvolte \'Zpět\' pro návrat do %1$s</string>
|
||||
<string name="prefs_in_ear_detection_summary">Přehrávání hovorů do sluchátek, jsou li v uších</string>
|
||||
<string name="heartrate_bpm_130">130 tepů/min</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">Text jako Bitmapa</string>
|
||||
<string name="pref_summary_banglejs_text_bitmap">Pokud není možno zobrazit dané slovo pomocí fontu hodinek, Gadgetbridge vykreslí obrázek, který se na hodinkách zobrazí</string>
|
||||
<string name="heartrate_bpm_135">135 tepů/min</string>
|
||||
<string name="heartrate_bpm_150">150 tepů/min</string>
|
||||
<string name="prefs_stress_monitoring_title">Sledování stresu</string>
|
||||
<string name="prefs_voice_detect_summary">Automaticky povolit okolní zvuk a snížit přehrávání po zjištění hlasu</string>
|
||||
<string name="pref_summary_device_intents">Umožní aplikacím Bangle.js hodinek posílat Android Intenty a povolí ostatním Android aplikacím (Tasker) aby posílaly data do Bangle.js pomocí com.banglejs.uart.tx Intentu.</string>
|
||||
<string name="prefs_heartrate_alert_experimental_description">Zavibruje řemínkem, pokud tepová frekvence překročí prahovou hodnotu bez zjevné fyzické aktivity v posledních 10 minutách. Tato funkce je experimentální a nebyla důkladně testována.</string>
|
||||
<string name="prefs_activity_monitoring_description">Automaticky zvýší četnost detekce srdeční frekvence při fyzické aktivitě.</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring">Monitorování srdeční frekvence</string>
|
||||
<string name="prefs_ambient_sound_during_call_title">Okolní zvuk během hovoru</string>
|
||||
<string name="prefs_switch_control_left">Ovládání přepínání Vlevo</string>
|
||||
<string name="pref_switch_controls_ambient_off">Okolní zvuk ←→ Vypnuto</string>
|
||||
<string name="heartrate_bpm_120">120 tepů/min</string>
|
||||
<string name="heartrate_bpm_140">140 tepů/min</string>
|
||||
<string name="heartrate_bpm_125">125 tepů/min</string>
|
||||
<string name="prefs_heartrate_alert_experimental_title">Upozornění na srdeční frekvenci (experimentální)</string>
|
||||
<string name="prefs_stress_monitoring_description">Sledování úrovně stresu při odpočinku</string>
|
||||
<string name="heartrate_bpm_145">145 tepů/min</string>
|
||||
<string name="prefs_heartrate_alert_threshold">Prahová hodnota upozornění na srdeční frekvenci</string>
|
||||
<string name="prefs_activity_monitoring_title">Sledování aktivity</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring_summary">Konfigurace monitorování srdečního tepu</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring_alerts_summary">Konfigurace monitorování srdečního tepu a prahových hodnot výstrah</string>
|
||||
<string name="prefs_seamless_connection_switch_title">Zjednodušené přepínání připojení</string>
|
||||
<string name="prefs_seamless_connection_switch_summary">Přepíná sluchátka automaticky mezi připojenými zařízeními</string>
|
||||
<string name="prefs_ambient_volume_left">Okolní hlasitost Vlevo</string>
|
||||
<string name="prefs_ambient_volume_right">Okolní hlasitost Vpravo</string>
|
||||
<string name="prefs_customize_ambient_sound_summary">Přizpůsobení nastavení okolního zvuku</string>
|
||||
<string name="prefs_ambient_sound_during_call_summary">Během hovoru slyšet vlastní hlas</string>
|
||||
<string name="prefs_ambient_settings_title">Možnosti okolního zvuku</string>
|
||||
<string name="prefs_active_noise_cancelling_level">Úroveň aktivního potlačení hluku</string>
|
||||
<string name="prefs_active_noise_cancelling_level_high">Vysoká</string>
|
||||
<string name="pref_title_touch_anc">Aktivní potlačení hluku</string>
|
||||
<string name="pref_switch_controls_anc_off">Potlačení hluku ←→ Vypnuto</string>
|
||||
<string name="prefs_voice_detect_duration">Konec po klidu za:</string>
|
||||
<string name="prefs_active_noise_cancelling_level_low">Nízká</string>
|
||||
<string name="pref_title_touch_quick_ambient">Rychlý okolní zvuk</string>
|
||||
<string name="prefs_noise_control_with_one_earbud">Regulace hluku s jedním sluchátkem</string>
|
||||
<string name="pref_ambient_sound_tone_summary">Od Měkkého po Jasný</string>
|
||||
<string name="pref_balance">Vyrovnání zvuku</string>
|
||||
<string name="pref_switch_controls_anc_ambient">Potlačení hluku ←→ Okolní zvuk</string>
|
||||
<string name="prefs_noise_control">Regulace hluku</string>
|
||||
<string name="prefs_voice_detect">Detekce hlasu</string>
|
||||
<string name="pref_title_touch_voice_assistant">Hlasový asistent</string>
|
||||
<string name="prefs_noise_control_with_one_earbud_summary">Povolit regulaci hluku i při použití pouze s jednoho sluchátka</string>
|
||||
<string name="pref_ambient_sound_tone">Tón okolního zvuku</string>
|
||||
<string name="prefs_double_tap_edge">Dvojité klepnutí na okraj</string>
|
||||
<string name="prefs_double_tap_edge_summary">Detekce dvojitého klepnutí, i když není klepnuto na dotykový panel</string>
|
||||
<string name="pref_voice_detect_duration_5">5 sekund</string>
|
||||
<string name="pref_voice_detect_duration_15">15 sekund</string>
|
||||
<string name="pref_voice_detect_duration_10">10 sekund</string>
|
||||
</resources>
|
@ -1627,4 +1627,5 @@
|
||||
<string name="gadgetbridge_running_banglejs_nopebble">Bangle.js läuft</string>
|
||||
<string name="about_activity_title_banglejs_nopebble">Über Bangle.js Gadgetbridge</string>
|
||||
<string name="pref_device_action_fitness_app_control_toggle">Fitness-App-Tracking umschalten</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">Text als Bitmaps</string>
|
||||
</resources>
|
@ -474,4 +474,11 @@
|
||||
<string name="devicetype_vesc">VESC</string>
|
||||
<string name="watchface_dialog_widget_width">Leveys:</string>
|
||||
<string name="watchface_dialog_widget_timezone">Aikavyöhyke:</string>
|
||||
<string name="pref_activity_recognition_mode_auto">automaattinen</string>
|
||||
<string name="pref_activity_recognize_rowing">tunnista soutaminen</string>
|
||||
<string name="pref_activity_recognize_running">tunnista juokseminen</string>
|
||||
<string name="menuitem_menu">Valikko</string>
|
||||
<string name="pref_activity_recognize_walking">tunnista käveleminen</string>
|
||||
<string name="pref_activity_recognition_mode_ask">kysy</string>
|
||||
<string name="pref_activity_recognize_biking">tunnista pyöräily</string>
|
||||
</resources>
|
@ -1699,4 +1699,10 @@
|
||||
<string name="prefs_ambient_sound_during_call_title">צליל הקפי במהלך שיחה</string>
|
||||
<string name="prefs_double_tap_edge_summary">לזהות נגיעה כפולה כשבוצעה בקצוות משטח המגע</string>
|
||||
<string name="prefs_heartrate_alert_experimental_description">להפעיל את הרטט בצמיד כאשר הדופק יורד מתחת לסף, ללא פעילות פיזית מובהקת ב־10 הדקות האחרונות. יכולת זאת נמצאת בשלבי ניסוי ולא נבדקה באופן קפדני.</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">טקסט כמפת סיביות</string>
|
||||
<string name="pref_title_device_internet_access">לאפשר גישה לאינטרנט</string>
|
||||
<string name="pref_summary_device_internet_access">לאפשר ליישומונים במכשיר הזה לגשת לאינטרנט</string>
|
||||
<string name="pref_summary_banglejs_text_bitmap">אם אי אפשר לעבד תמונות עם גופן השעון, הוא יומר למפת סיביות ב־Gadgetbridge שתוצג בשעון</string>
|
||||
<string name="pref_summary_device_intents">לאפשר ליישומוני שעון של Bangle.js לשלוח Intents ל־Android ולאפשר ליישומי Android אחרים (כמו Tasker) לשלוח נתונים ל־Bangle.js באמצעות ה־Intent com.banglejs.uart.tx.</string>
|
||||
<string name="pref_title_device_intents">לאפשר Intents</string>
|
||||
</resources>
|
@ -9,12 +9,12 @@
|
||||
<string name="controlcenter_fetch_activity_data">Synchroniseer</string>
|
||||
<string name="controlcenter_find_device">Zoek verloren apparaat</string>
|
||||
<string name="controlcenter_take_screenshot">Screenshot maken</string>
|
||||
<string name="controlcenter_disconnect">Ontkoppel</string>
|
||||
<string name="controlcenter_disconnect">Verbinding verbreken</string>
|
||||
<string name="controlcenter_delete_device">Verwijder apparaat</string>
|
||||
<string name="controlcenter_delete_device_name">Verwijder %1$s</string>
|
||||
<string name="controlcenter_delete_device_dialogmessage">Dit zal het apparaat en alle bijbehorende gegevens verwijderen!</string>
|
||||
<string name="controlcenter_snackbar_need_longpress">Druk lang op de kaart om te ontkoppelen</string>
|
||||
<string name="controlcenter_snackbar_disconnecting">Ontkoppelen</string>
|
||||
<string name="controlcenter_snackbar_need_longpress">Druk lang op de kaart om de verbinding te verbreken</string>
|
||||
<string name="controlcenter_snackbar_disconnecting">Verbinding verbreken</string>
|
||||
<string name="controlcenter_snackbar_connecting">Verbinden…</string>
|
||||
<string name="controlcenter_snackbar_requested_screenshot">Een screenshot maken van het apparaat</string>
|
||||
<string name="title_activity_debug">Debug</string>
|
||||
@ -1698,4 +1698,12 @@
|
||||
<string name="heartrate_bpm_150">150 bpm</string>
|
||||
<string name="heartrate_bpm_135">135 bpm</string>
|
||||
<string name="heartrate_bpm_140">140 bpm</string>
|
||||
<string name="pref_summary_device_internet_access">Apps op dit apparaat toegang geven tot internet</string>
|
||||
<string name="pref_summary_device_intents">Sta toe dat Bangle.js apps Android-intents verzenden en sta andere apps op Android (zoals Tasker) toe om gegevens naar Bangle.js te verzenden met de com.banglejs.uart.tx-intent.</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">Tekst als afbeelding</string>
|
||||
<string name="pref_title_device_internet_access">Internettoegang toestaan</string>
|
||||
<string name="pref_summary_banglejs_text_bitmap">Als een woord niet kan worden weergegeven met het lettertype van het horloge, render het dan naar een afbeelding in Gadgetbridge en toon de afbeelding op het horloge</string>
|
||||
<string name="pref_title_device_intents">Intents toestaan</string>
|
||||
<string name="prefs_switch_control_left">Bediening links</string>
|
||||
<string name="prefs_switch_control_right">Bediening rechts</string>
|
||||
</resources>
|
@ -1606,4 +1606,39 @@
|
||||
<string name="activity_db_management_autoexport_location">Lokalizacja nie jest dostępna. Najprawdopodobniej jest to spowodowane nowym systemem uprawnień Androida. Prawdopodobnie automatyczny eksport teraz nie działa.</string>
|
||||
<string name="watchface_setting_light_up_on_notification">Podświetlanie nowych powiadomień</string>
|
||||
<string name="menuitem_email">E-mail</string>
|
||||
<string name="pref_screen_notification_profile_find_device">Znajdź urządzenie</string>
|
||||
<string name="pref_screen_notification_profile_event_reminder">Przypomnienie o wydarzeniach</string>
|
||||
<string name="pref_screen_notification_idle_alerts">Alerty o bezczynności</string>
|
||||
<string name="pref_screen_vibration_patterns_title">Wzory wibracji</string>
|
||||
<string name="pref_screen_vibration_patterns_summary">Skonfiguruj wzory wibracji dla różnych powiadomień</string>
|
||||
<string name="devicetype_sony_wf_1000xm3">Sony WF-1000XM3</string>
|
||||
<string name="devicetype_galaxybuds_pro">Galaxy Buds Pro</string>
|
||||
<string name="about_activity_title_banglejs_main">O Bangle.js Gadgetbridge</string>
|
||||
<string name="application_name_banglejs_main">Bangle.js Gadgetbridge</string>
|
||||
<string name="title_activity_controlcenter_banglejs_main">Bangle.js Gadgetbridge</string>
|
||||
<string name="gadgetbridge_running_banglejs_main">Bangle.js jest uruchomiony</string>
|
||||
<string name="about_activity_title_banglejs_nopebble">O Bangle.js Gadgetbridge</string>
|
||||
<string name="application_name_banglejs_nopebble">Bangle.js Gadgetbridge</string>
|
||||
<string name="title_activity_controlcenter_banglejs_nopebble">Bangle.js Gadgetbridge</string>
|
||||
<string name="gadgetbridge_running_banglejs_nopebble">Bangle.js jest uruchomiony</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">Tekst jako bitmapy</string>
|
||||
<string name="pref_title_device_internet_access">Zezwól na dostęp do internetu</string>
|
||||
<string name="pref_summary_device_internet_access">Zezwól aplikacjom na tym urządzeniu na dostęp do Internetu</string>
|
||||
<string name="heartrate_bpm_100">100 bpm</string>
|
||||
<string name="world_clock_delete_confirm_title">Usuń \'%1$s\'</string>
|
||||
<string name="world_clock_timezone">Strefa czasowa</string>
|
||||
<string name="heartrate_bpm_105">105 bpm</string>
|
||||
<string name="heartrate_bpm_110">110 bpm</string>
|
||||
<string name="heartrate_bpm_112">112 bpm</string>
|
||||
<string name="heartrate_bpm_120">120 bpm</string>
|
||||
<string name="heartrate_bpm_125">125 bpm</string>
|
||||
<string name="heartrate_bpm_130">130 bpm</string>
|
||||
<string name="heartrate_bpm_135">135 bpm</string>
|
||||
<string name="heartrate_bpm_140">140 bpm</string>
|
||||
<string name="heartrate_bpm_145">145 bpm</string>
|
||||
<string name="heartrate_bpm_150">150 bpm</string>
|
||||
<string name="pref_title_touch_spotify">Spotify</string>
|
||||
<string name="pref_voice_detect_duration_5">5 sekund</string>
|
||||
<string name="pref_voice_detect_duration_10">10 sekund</string>
|
||||
<string name="pref_voice_detect_duration_15">15 sekund</string>
|
||||
</resources>
|
@ -1716,4 +1716,10 @@
|
||||
<string name="mi2_prefs_heart_rate_monitoring_alerts_summary">Kalp ritmi izlemeyi ve uyarı eşiklerini yapılandırın</string>
|
||||
<string name="prefs_seamless_connection_switch_summary">Kulaklıkları eşleştirilen aygıtlar arasında otomatik olarak değiştirir</string>
|
||||
<string name="prefs_ambient_volume_right">Ortam Ses Seviyesi Sağ</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">Bit Eşlem Olarak Metin</string>
|
||||
<string name="pref_summary_device_intents">Bangle.js saat uygulamalarının Android Amaçları göndermesine ve Android\'deki diğer uygulamaların (Tasker gibi) com.banglejs.uart.tx Niyeti ile Bangle.js\'ye veri göndermesine izin ver.</string>
|
||||
<string name="pref_title_device_intents">Amaçlara İzin Ver</string>
|
||||
<string name="pref_summary_device_internet_access">Bu aygıttaki uygulamaların internete erişmesine izin ver</string>
|
||||
<string name="pref_summary_banglejs_text_bitmap">Bir sözcük saatin yazı tipi kullanılarak görüntülenemiyorsa, onu Gadgetbridge\'de bir bit eşleme dönüştür ve bit eşlemi saatte görüntüle</string>
|
||||
<string name="pref_title_device_internet_access">İnternet Erişimine İzin Ver</string>
|
||||
</resources>
|
@ -1707,4 +1707,10 @@
|
||||
<string name="mi2_prefs_heart_rate_monitoring_alerts_summary">Налаштуйте пороги стеження за частотою серцевих скорочень та попереджень</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring">Стеження за частотою серцевих скорочень</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring_summary">Налаштувати стеження за частотою серцевих скорочень</string>
|
||||
<string name="pref_title_device_internet_access">Дозволити доступ до інтернету</string>
|
||||
<string name="pref_summary_device_internet_access">Дозволити застосункам на цьому пристрої доступ до інтернету</string>
|
||||
<string name="pref_summary_device_intents">Дозволити застосункам для годинника Bangle.js надсилати наміри Android і дозволити іншим застосункам на Android (наприклад, Tasker) надсилати дані до Bangle.js із наміром com.banglejs.uart.tx.</string>
|
||||
<string name="pref_title_device_intents">Дозволити наміри</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">Текст у вигляді растрових зображень</string>
|
||||
<string name="pref_summary_banglejs_text_bitmap">Якщо слово не може бути відтворено шрифтом годинника, перетворювати його на растрове зображення в Gadgetbridge і показувати растрове зображення на годиннику</string>
|
||||
</resources>
|
@ -1705,4 +1705,10 @@
|
||||
<string name="mi2_prefs_heart_rate_monitoring">心率监测</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring_summary">配置心率监测</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring_alerts_summary">配置心率监测和警报阈值</string>
|
||||
<string name="pref_title_banglejs_text_bitmap">作为位图的文本</string>
|
||||
<string name="pref_summary_banglejs_text_bitmap">如果无法使用手表的字体呈现单词,请将其呈现为 Gadgetbridge 中的位图,并在手表上显示位图</string>
|
||||
<string name="pref_summary_device_intents">允许 Bangle.js 手表应用发送 Android 意图,并允许 Android 上的其他应用(如 Tasker)使用 com.banglejs.uart.tx 意图向 Bangle.js 发送数据。</string>
|
||||
<string name="pref_title_device_intents">允许意向</string>
|
||||
<string name="pref_title_device_internet_access">允许互联网访问</string>
|
||||
<string name="pref_summary_device_internet_access">允许此设备上的应用访问互联网</string>
|
||||
</resources>
|
@ -462,6 +462,7 @@
|
||||
<item>@string/menuitem_activity</item>
|
||||
<item>@string/menuitem_eventreminder</item>
|
||||
<item>@string/menuitem_pai</item>
|
||||
<item>@string/menuitem_sleep</item>
|
||||
<item>@string/menuitem_worldclock</item>
|
||||
<item>@string/menuitem_stress</item>
|
||||
<item>@string/menuitem_cycles</item>
|
||||
@ -489,6 +490,7 @@
|
||||
<item>@string/p_menuitem_activity</item>
|
||||
<item>@string/p_menuitem_eventreminder</item>
|
||||
<item>@string/p_menuitem_pai</item>
|
||||
<item>@string/p_menuitem_sleep</item>
|
||||
<item>@string/p_menuitem_worldclock</item>
|
||||
<item>@string/p_menuitem_stress</item>
|
||||
<item>@string/p_menuitem_cycles</item>
|
||||
@ -516,6 +518,7 @@
|
||||
<item>@string/p_menuitem_activity</item>
|
||||
<item>@string/p_menuitem_eventreminder</item>
|
||||
<item>@string/p_menuitem_pai</item>
|
||||
<item>@string/p_menuitem_sleep</item>
|
||||
<item>@string/p_menuitem_worldclock</item>
|
||||
<item>@string/p_menuitem_stress</item>
|
||||
<item>@string/p_menuitem_cycles</item>
|
||||
@ -540,6 +543,7 @@
|
||||
<item>@string/menuitem_mutephone</item>
|
||||
<item>@string/menuitem_eventreminder</item>
|
||||
<item>@string/menuitem_pai</item>
|
||||
<item>@string/menuitem_sleep</item>
|
||||
<item>@string/menuitem_worldclock</item>
|
||||
<item>@string/menuitem_stress</item>
|
||||
<item>@string/menuitem_cycles</item>
|
||||
@ -565,6 +569,7 @@
|
||||
<item>@string/p_menuitem_mutephone</item>
|
||||
<item>@string/p_menuitem_eventreminder</item>
|
||||
<item>@string/p_menuitem_pai</item>
|
||||
<item>@string/p_menuitem_sleep</item>
|
||||
<item>@string/p_menuitem_worldclock</item>
|
||||
<item>@string/p_menuitem_stress</item>
|
||||
<item>@string/p_menuitem_cycles</item>
|
||||
|
@ -372,6 +372,10 @@
|
||||
<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_start_on_phone_title">Fitness app tracking</string>
|
||||
<string name="pref_workout_start_on_phone_summary">Start/stop fitness app tracking on phone when a GPS workout is started on the band</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>
|
||||
@ -1031,6 +1035,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>
|
||||
@ -1627,6 +1634,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 -->
|
||||
|
@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<changelog>
|
||||
<release version="0.67.1" versioncode="212">
|
||||
<change>Huami: Fix long music track names not displaying</change>
|
||||
<change>Amazfit Bip U/Pro/Band 5: Enable extended HR/stress monitoring setting</change>
|
||||
<change>Pebble: Fix calendar blacklist, view and storage</change>
|
||||
<change>FitPro: Fix crash, inactivity warning preference to string </change>
|
||||
</release>
|
||||
<release version="0.67.0" versioncode="211">
|
||||
<change>Initial Support for Sony WF-1000XM3</change>
|
||||
<change>Initial Support for Galaxy Buds Pro</change>
|
||||
|
@ -21,7 +21,7 @@
|
||||
android:dependency="inactivity_warnings_enable"
|
||||
android:entries="@array/inactivity_minutes"
|
||||
android:entryValues="@array/inactivity_minutes_values"
|
||||
android:key="inactivity_warnings_threshold"
|
||||
android:key="inactivity_warnings_threshold_extended"
|
||||
android:summary="@string/mi2_prefs_inactivity_warnings_summary"
|
||||
android:title="@string/mi2_prefs_inactivity_warnings_threshold" />
|
||||
|
||||
|
@ -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>
|
@ -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_activity_unknown_small"
|
||||
android:key="workout_start_on_phone"
|
||||
android:summary="@string/pref_workout_start_on_phone_summary"
|
||||
android:title="@string/pref_workout_start_on_phone_title" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -6,4 +6,6 @@
|
||||
android:minWidth="40dp"
|
||||
android:previewImage="@drawable/ic_launcher"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:widgetCategory="home_screen"></appwidget-provider>
|
||||
android:configure="nodomain.freeyourgadget.gadgetbridge.activities.SleepAlarmWidgetConfigurationActivity"
|
||||
android:widgetCategory="home_screen">
|
||||
</appwidget-provider>
|
@ -19,12 +19,17 @@ public class CalendarEventTest extends TestBase {
|
||||
private static final long ID_1 = 100;
|
||||
private static final long ID_2 = 101;
|
||||
private static final String CALNAME_1 = "cal1";
|
||||
private static final String CALACCOUNTNAME_1 = "account1";
|
||||
private static final int COLOR_1 = 185489;
|
||||
|
||||
@Test
|
||||
public void testHashCode() {
|
||||
CalendarEvents.CalendarEvent c1 = new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, false);
|
||||
CalendarEvents.CalendarEvent c2 = new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, false);
|
||||
CalendarEvents.CalendarEvent c3 = new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, false);
|
||||
public void testHashCode() {
|
||||
CalendarEvents.CalendarEvent c1 =
|
||||
new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
|
||||
CalendarEvents.CalendarEvent c2 =
|
||||
new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
|
||||
CalendarEvents.CalendarEvent c3 =
|
||||
new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
|
||||
|
||||
assertEquals(c1.hashCode(), c1.hashCode());
|
||||
assertNotEquals(c1.hashCode(), c2.hashCode());
|
||||
@ -35,7 +40,7 @@ public class CalendarEventTest extends TestBase {
|
||||
@Test
|
||||
public void testSync() {
|
||||
List<CalendarEvents.CalendarEvent> eventList = new ArrayList<>();
|
||||
eventList.add(new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, false));
|
||||
eventList.add(new CalendarEvents.CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
|
||||
|
||||
GBDevice dummyGBDevice = createDummyGDevice("00:00:01:00:03");
|
||||
dummyGBDevice.setState(GBDevice.State.INITIALIZED);
|
||||
@ -44,7 +49,7 @@ public class CalendarEventTest extends TestBase {
|
||||
|
||||
testCR.syncCalendar(eventList);
|
||||
|
||||
eventList.add(new CalendarEvents.CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, false));
|
||||
eventList.add(new CalendarEvents.CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
|
||||
testCR.syncCalendar(eventList);
|
||||
|
||||
CalendarSyncStateDao calendarSyncStateDao = daoSession.getCalendarSyncStateDao();
|
||||
|
2
external/fossil-hr-watchface
vendored
2
external/fossil-hr-watchface
vendored
@ -1 +1 @@
|
||||
Subproject commit aad2a141cb2e151431f8354e52d9b74f6829855a
|
||||
Subproject commit f07ed376e9046dbcc9c5d7821117c80b2d79ffd1
|
4
fastlane/metadata/android/en-US/changelogs/212.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/212.txt
Normal file
@ -0,0 +1,4 @@
|
||||
* Huami: Fix long music track names not displaying
|
||||
* Amazfit Bip U/Pro/Band 5: Enable extended HR/stress monitoring setting
|
||||
* Pebble: Fix calendar blacklist, view and storage
|
||||
* FitPro: Fix crash, inactivity warning preference to string
|
Loading…
Reference in New Issue
Block a user