1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-09-08 15:35:24 +02:00

Merge branch 'master' of codeberg.org:Freeyourgadget/Gadgetbridge

This commit is contained in:
Gordon Williams 2022-06-10 10:15:20 +01:00
commit 6fb22b9441
76 changed files with 1679 additions and 353 deletions

View File

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

View File

@ -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()}\""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,8 +35,9 @@ public class HuamiService {
public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString("00001532-0000-3512-2118-0009af100700");
public static final UUID UUID_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");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -65,6 +65,7 @@ public class GB {
public static final String NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID = "gadgetbridge_high_priority";
public static final String NOTIFICATION_CHANNEL_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);

View File

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

View File

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

View File

@ -256,6 +256,14 @@
android:text="Show Fit.App.Track. Status"
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_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>

View File

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

View File

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

@ -1 +1 @@
Subproject commit aad2a141cb2e151431f8354e52d9b74f6829855a
Subproject commit f07ed376e9046dbcc9c5d7821117c80b2d79ffd1

View 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