diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e9976c1..9d29d9d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 76d42bd87..15a94424f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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()}\"" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dd1335963..0e0ab834b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -595,6 +595,12 @@ + + + + + + diff --git a/app/src/main/assets/fossil_hr/openSourceWatchface.bin b/app/src/main/assets/fossil_hr/openSourceWatchface.bin index 41e239c15..6358d3cdf 100644 Binary files a/app/src/main/assets/fossil_hr/openSourceWatchface.bin and b/app/src/main/assets/fossil_hr/openSourceWatchface.bin differ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 085370cb4..82a6e166a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -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 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 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 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(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java index 11b054843..c74d3adc7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java index 0cbfef767..8121d80dd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java @@ -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 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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/CalBlacklistActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/CalBlacklistActivity.java index cb2744ab8..cfdf1afbf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/CalBlacklistActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/CalBlacklistActivity.java @@ -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 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 { @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java index 640d12359..0e9a24e0e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java @@ -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() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index 29957ada4..d34ec7760 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SleepAlarmWidgetConfigurationActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SleepAlarmWidgetConfigurationActivity.java new file mode 100644 index 000000000..a2e623549 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SleepAlarmWidgetConfigurationActivity.java @@ -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> 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 list = new ArrayList<>(); + for (Map.Entry> 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> selectedItem = + (Map.Entry>) 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> newMap = new LinkedHashMap<>(1); + List 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java index 59ee7ff21..f078a6f21 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java @@ -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); - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetConfigurationActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetConfigurationActivity.java index 8ffea8c00..b89d83ce9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetConfigurationActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetConfigurationActivity.java @@ -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> 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, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java index 9ed08de5d..0a74095c3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java @@ -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; } -} \ No newline at end of file + + @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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index d2ff3f607..a07af38d8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -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"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index efe23cbca..53808daff 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index a0ec99c1c..f8674afd8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -18,6 +18,7 @@ along with this program. If not, see . */ 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); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java index 42575cf63..cc517c788 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java index d2a07bb41..9742aee5b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java @@ -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"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java index 763c166e7..227b15b12 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java @@ -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, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java index 0c40d3489..5eff096c1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java @@ -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, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java index 5e0d74fea..301df6982 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/AbstractLocationProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/AbstractLocationProvider.java new file mode 100644 index 000000000..a361dfca0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/AbstractLocationProvider.java @@ -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 . */ +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); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/GBLocationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/GBLocationListener.java new file mode 100644 index 000000000..edd9c61ab --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/GBLocationListener.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/GBLocationManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/GBLocationManager.java new file mode 100644 index 000000000..c657c0017 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/GBLocationManager.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/MockLocationProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/MockLocationProvider.java new file mode 100644 index 000000000..60f3f75bf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/MockLocationProvider.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/PhoneGpsLocationProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/PhoneGpsLocationProvider.java new file mode 100644 index 000000000..88880a4eb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/gps/PhoneGpsLocationProvider.java @@ -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 . */ +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()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/OpenTracksContentObserver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/OpenTracksContentObserver.java new file mode 100644 index 000000000..cd2374338 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/OpenTracksContentObserver.java @@ -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 . */ + +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 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; + } + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/OpenTracksController.java similarity index 96% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/OpenTracksController.java index 93a6d9892..3f1f590a4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksController.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/OpenTracksController.java @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -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) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksContentObserver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/Track.java similarity index 55% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksContentObserver.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/Track.java index aee21b9da..6b9032472 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OpenTracksContentObserver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/Track.java @@ -15,15 +15,11 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -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 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 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; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/TrackStatistics.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/TrackStatistics.java new file mode 100644 index 000000000..3e40583b8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/opentracks/TrackStatistics.java @@ -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 . */ + +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index d0f12cc73..ff04bde7c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -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); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java index 9580bfc01..776ac299b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index bd22e9af3..c653dae77 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -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"; /** diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index d83db85c6..defa78adf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -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; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index 37224e887..fe6fffd30 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -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); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index 26c1a587c..28cabe7c4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -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 reminders) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java index d46ffa76a..10bd6452f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java @@ -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": { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java index 63596edfa..a35bfabca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java @@ -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(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java index 5ac91e11f..c299eb845 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiPhoneGpsStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiPhoneGpsStatus.java new file mode 100644 index 000000000..c855f12d7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiPhoneGpsStatus.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index e421d9ea2..debbf75c0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutScreenActivityType.java similarity index 75% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutActivityType.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutScreenActivityType.java index 70688310c..a5a5906c2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutActivityType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutScreenActivityType.java @@ -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); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutStatus.java new file mode 100644 index 000000000..651fb320a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutStatus.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutTrackActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutTrackActivityType.java new file mode 100644 index 000000000..0757d70c6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiWorkoutTrackActivityType.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java index 734acc1cd..74705d0f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6FirmwareInfo.java index e4d26c65d..ec47bacea 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6FirmwareInfo.java @@ -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) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java index bdd0a63f6..711aa3251 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java @@ -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) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java index fc69b4e39..8bb941f06 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/workout/WorkoutRequestHandler.java @@ -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 { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index 245f5c5d7..5e5150b3e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -17,6 +17,8 @@ along with this program. If not, see . */ 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); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index e2f9005da..580d56292 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -17,6 +17,8 @@ along with this program. If not, see . */ 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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index d2cbd09bb..53b7c1c51 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java index bcbdf8fb7..efcd7c807 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java @@ -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 devices = gbApp.getDeviceManager().getDevices(); + for (GBDevice device : devices) { + if (device.getAddress().equals(HwAddress)) { + return device; + } + } + return null; + } + + } diff --git a/app/src/main/res/drawable/ic_gps_location.xml b/app/src/main/res/drawable/ic_gps_location.xml new file mode 100644 index 000000000..8173d3d4b --- /dev/null +++ b/app/src/main/res/drawable/ic_gps_location.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml index 19de06c28..ec93917b2 100644 --- a/app/src/main/res/layout/activity_debug.xml +++ b/app/src/main/res/layout/activity_debug.xml @@ -256,6 +256,14 @@ android:text="Show Fit.App.Track. Status" grid:layout_columnSpan="2" grid:layout_gravity="fill_horizontal" /> + +