Compare commits

..

51 Commits

Author SHA1 Message Date
José Rebelo ff51fa7309 Garmin HACK: Parse messageTypes > 0x8000 2024-04-25 18:17:56 +01:00
José Rebelo 81ea814681 Garmin: Send location to watch 2024-04-25 18:14:07 +02:00
Daniele Gobbetti 2605dff105 Garmin: calendar integration improvements
use the protobuf fields described in the documentation[0]
build the message according to the requested fields

[0] https://gadgetbridge.org/internals/specifics/garmin-protocol/#calendarevent
2024-04-25 18:09:36 +02:00
Daniele Gobbetti 39aea72ef7 Garmin: fixup use developer device setting for keeping data on device
Logic was inverted
2024-04-25 18:09:36 +02:00
a0z 60c3834e0b Garmin: Initial support of Instinct 2 Solar 2024-04-25 18:09:36 +02:00
Daniele Gobbetti 6e0d7edfa7 Garmin: fix notification crashes and handle SMS correctly
It looks like (some) watches really don't like having an empty list of actions, hence enable the legacy "refuse" action in every case, leaving it empty and inactive.
Further display the SMS sender in the notification and enable the correct code path for the reply action to work.
2024-04-25 18:09:36 +02:00
José Rebelo 025f15e0fa Garmin: Auto-detect canned messages support 2024-04-25 18:09:36 +02:00
José Rebelo db378faf47 Garmin: Fix reply to sms 2024-04-25 18:09:36 +02:00
José Rebelo 7bf73c6b41 Garmin: Add setting to disable notifications 2024-04-25 18:09:36 +02:00
José Rebelo f6754df211 Garmin Venu 3: Enable canned replies 2024-04-25 18:09:36 +02:00
Daniele Gobbetti b4414f0799 Garmin: use developer device setting for keeping data on device
Make use of the previously added preference to toggle file archival (deletion) on the watch.

Default is true (keep data on device) until we are sure of the consequences.
2024-04-25 18:09:36 +02:00
José Rebelo ce0a61e211 Garmin: Map all known files types 2024-04-25 18:09:36 +02:00
José Rebelo 3386e86158 Garmin: Add support for http weather requests 2024-04-25 18:09:36 +02:00
Daniele Gobbetti c5a94d2927 Garmin: Rename LocalMessage to PredefinedLocalMessage and clarify its usage
PredefinedLocalMessage are only useful for FIT messages and should not interfere with FIT files. The only impact of using the local message in fit files was in the textual output, but it was confusing.

Add an explicit constructor to RecordHeader if PredefinedLocalMessage should be taken into account, and use this only in fit messages leaving the default constructor for fit files.

Also adjusts the test case as textual output comparison needs to be fixed.
2024-04-25 18:09:36 +02:00
kuhy 18a6a6b1c7 Initial support for Garmin Vivoactive 4S 2024-04-25 18:09:36 +02:00
Daniele Gobbetti f1c7c97558 Garmin: Add support for custom replies (notifications and calls)
To enable custom replies an override must be defined in the devices coordinator that actually support custom replies.

The custom preferences allow to:
- enable / disable the default message suffix (Instinct 2 appends "sent from my $vendor device" to each reply by default)
- define custom messages to reply to calls and incoming messages (leaving those lists empty will enable the default messages to be used)

Also adds a new protobuf definition file of mostly unknown values that enable toggling the message suffix on Instinct 2.
2024-04-25 18:09:36 +02:00
myxor 3baeb2b14e Initial support for Garmin Vivoactive 5 2024-04-25 18:09:36 +02:00
Daniele Gobbetti d6ade723d3 Garmin: Add support for replying to notifications
This uses the (assumed) new method of passing multiple actions, instead of the (assumed) legacy accept/decline approach.
At the moment the preset messages stored on the watch firmware are used for replying, the code supports using custom messages already but those have to be updated to the watch somehow (probably by protobuf) and this is not supported yet. Using custom messages if they are not set will just do nothing.
The NotificationActionIconPosition values have been determined on a vĂ­vomove Style and might not work properly on other watches.
The evaluation of GBDeviceEvent have been moved in GarminSupport since the notification actions handling uses device events.

Also adds a method to read null terminated strings to GarminByteBufferReader.
Also adds a warning in NotificationListener if the wrong handle is used for replying to a notification.
2024-04-25 18:09:36 +02:00
Daniele Gobbetti 863c1a5657 Garmin: Add FileDownloadedDeviceEvent and (disabled) file deletion
Also adds (disabled) file deletion in case of already downloaded files
2024-04-25 18:09:36 +02:00
Daniele Gobbetti 16f5890a95 Garmin: Add DST/Timezone support 2024-04-25 18:09:36 +02:00
hrdl 10c5286ec1 Add Garmin Forerunner 245 2024-04-25 18:09:36 +02:00
Daniele Gobbetti 6b821a2f1f Garmin: Support file archival (deletion) on watch
Also add original timestamp to local cache filename as the file identifier are reused
Also fix imports of Test class
2024-04-25 18:09:36 +02:00
José Rebelo bcfaf7b3e8 Garmin: Fetch activity on demand 2024-04-25 18:09:36 +02:00
José Rebelo 2f2b95beee Garmin: Fix proguard rules for release builds 2024-04-25 18:09:36 +02:00
José Rebelo 6569cd74ba Garmin: Allow high MTU 2024-04-25 18:09:36 +02:00
José Rebelo 49d4792677 Garmin protocol: Simplify FILE_TYPE 2024-04-25 18:09:36 +02:00
José Rebelo 418bb7d37a Garmin protocol: Fix linter warnings 2024-04-25 18:09:36 +02:00
José Rebelo 044ac1e917 Garmin protocol: Introduce GarminCoordinator 2024-04-25 18:09:36 +02:00
José Rebelo bad5cd4045 Garmin protocol: fix crash when stopping find phone 2024-04-25 18:09:35 +02:00
Daniele Gobbetti b3da377b34 Garmin protocol: basic file transfer and notification handling
adds synchronization of supported files from watch to external directory
adds support for Activity and Monitoring files (workouts and activity samples), but those are not integrated yet
adds upload functionality (not used ATM and not tested)
adds notification support without actions
introduces centralized processing of "messageHandlers" (protobuf, file transfer, notifications)

also properly dispose of the music timer when disconnecting
2024-04-25 18:09:35 +02:00
Daniele Gobbetti 790019fa5a Garmin protocol: enable media volume control from watch 2024-04-25 18:09:35 +02:00
Daniele Gobbetti c96d1da1e5 Garmin protocol: store max packet size from DeviceInformationMessage
also adds messageType to the warnifleftover log message
2024-04-25 18:09:35 +02:00
Daniele Gobbetti 293449b5e0 Garmin protocol: various changes
- add FitFile class that deals with parsing and generating outgoing files
- consider all field definitions with number 253 as Timestamps [0]
- add support for "compressed timestamps" in fit file parsing. Those are not returned among the other normal fields but are available through a method of RecordData
- adjust the test cases

[0]48b6554d8a/fitdecode/reader.py (L719)
2024-04-25 18:09:35 +02:00
Daniele Gobbetti 57db0c7c33 Garmin protocol: change naming and logic of several FIT classes
- refactor the logic of Global and Local messages
- add some Global messages with naming taken from [1]
- Global messages are not enum because there are too many
- introduce the concept of FieldDefinitionPrimitive
- add new Field Definitions
- add support for developer fields and array fields
- add test case for FIT files taken from [0]

[0] https://github.com/polyvertex/fitdecode/
[1] https://www.fitfileviewer.com/
2024-04-25 18:09:35 +02:00
Daniele Gobbetti 379b8912cb Garmin protocol: create helper class GarminByteBufferReader
separate the logic specific for GFDI messages from the generally useful logic.
Also centralize the logging in case of leftover bytes while parsing GFDI messages.
2024-04-25 18:09:35 +02:00
Daniele Gobbetti 2aa8667998 Garmin protocol: create custom GBDeviceEvent for weather request 2024-04-25 18:09:35 +02:00
Daniele Gobbetti 3a3ff5bc6a Garmin protocol: use message enum instead of id in GFDI Messages 2024-04-25 18:09:35 +02:00
Daniele Gobbetti eff233d93a Garmin protocol: refactoring and fixes of BaseTypes
The boundaries are enforced on the stored value when decoding, before applying the adjustments for scale and offset.
Also add some tests for the BaseTypes
Introduce new FieldDefinition for Temperature and WeatherCondition (removing the static class)
Add accessors for field data in the containing RecordData, thus keeping the FieldData private
2024-04-25 18:09:35 +02:00
Daniele Gobbetti e73d4a7130 Garmin protocol: create specific field definition for day of week 2024-04-25 18:09:35 +02:00
Daniele Gobbetti 40d064cf7f Garmin protocol: move field encode/decode interface to the FieldDefinition
This allows for semantic subclassing the FieldDefinition.
A FieldDefinitionTimestamp subclass is introduced as example
2024-04-25 18:09:35 +02:00
Daniele Gobbetti 719a104811 Garmin protocol: fix invalid signed int base type value 2024-04-25 18:09:35 +02:00
Daniele Gobbetti fb3338f099 Garmin protocol: add initial support for FIT messages
note: only weather message definition and data tested so far
also enable weather support for Instinct 2S and vivomove style
also cleanup some unused constants that have been migrated to new enums in GFDIMessage
additionally switch to new local implementation of GarminTimeUtils with needed methods
2024-04-25 18:09:35 +02:00
Daniele Gobbetti fe1f610546 Garmin protocol: fixes
- fix DEVICE_SETTINGS message ID
- put all status messages in own package
- allow protobuf handler to change the returned status message to signal unsupported requests
- fix various bugs
2024-04-25 18:09:35 +02:00
Daniele Gobbetti 1d1c6146a7 Garmin protocol: initial refactoring and basic functionalities
This commit takes aims to bring many new garmin devices up to a working status, with basic functionalities such as:
- garmin protocol initialization
- basic message exchange
- support for some messages in Garmin own format
- support for some messages in protobuf format
2024-04-25 18:09:35 +02:00
José Rebelo 500e930237 Refactor location service
- Refactor the code from a static global instance to a lifecycle-aware
  service instantiated in the DeviceCommunicationService
- Fix number of devices reported in the notification
- Prevents leaks and properly stops when devices get disconnected
2024-04-25 17:08:53 +01:00
José Rebelo 3799ffb72c Zepp OS: Sync calendar event reminders 2024-04-25 15:58:57 +01:00
José Rebelo 13d6c49bb5 Xiaomi: Sync calendar event reminders 2024-04-25 15:00:48 +01:00
Vitaliy Tomin 67cf9b2f00 huawei: Add huawei account support (#3721)
* this feature allows to pair HarmonyOS devices without factory reset to
  GB and Huawei Health.

* huawei account has form of 17 digit string and could be retrived from
  logcat filtering by huid=

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3721
Co-authored-by: Vitaliy Tomin <highwaystar.ru@gmail.com>
Co-committed-by: Vitaliy Tomin <highwaystar.ru@gmail.com>
2024-04-25 12:19:00 +00:00
Daniele Gobbetti 173e2d29b0 Include Organizer and Reminders when reading calendar events
Also use the named column indexes instead of numeric ids when retrieving the contents to make it more clear and more robust in case further fields are added later.

Reminders are set as absolute timestamp.
2024-04-25 11:46:34 +02:00
Marcel Alexandru Nitan 2190c82ed7 feature: Sleep as android support
Implement support for Sleep As Android with an usable example for ZeppOs
devices

Sleep as Android documentation:

https://docs.sleep.urbandroid.org/devs/wearable_api.html

Signed-off-by: Marcel Alexandru Nitan <nitan.marcel@protonmail.com>
2024-04-20 12:15:42 +03:00
Arjan Schrijver f186053dab Dashboard: Add preference for drawing midnight at bottom of 24h chart 2024-04-18 20:06:56 +02:00
73 changed files with 2072 additions and 537 deletions

View File

@ -1,2 +0,0 @@
connection.project.dir=
eclipse.preferences.version=1

View File

@ -110,6 +110,24 @@
android:requestLegacyExternalStorage="true"
android:theme="@style/GadgetbridgeTheme"
tools:replace="android:label">
<receiver
android:name=".externalevents.sleepasandroid.SleepAsAndroidReceiver"
android:enabled="true"
android:exported="true" >
<intent-filter>
<action android:name="com.urbandroid.sleep.watch.START_TRACKING" />
<action android:name="com.urbandroid.sleep.watch.STOP_TRACKING" />
<action android:name="com.urbandroid.sleep.watch.SET_PAUSE" />
<action android:name="com.urbandroid.sleep.watch.SET_SUSPENDED" />
<action android:name="com.urbandroid.sleep.watch.SET_BATCH_SIZE" />
<action android:name="com.urbandroid.sleep.watch.START_ALARM" />
<action android:name="com.urbandroid.sleep.watch.STOP_ALARM" />
<action android:name="com.urbandroid.sleep.watch.UPDATE_ALARM" />
<action android:name="com.urbandroid.sleep.watch.SHOW_NOTIFICATION" />
<action android:name="com.urbandroid.sleep.watch.HINT" />
<action android:name="com.urbandroid.sleep.watch.CHECK_CONNECTED" />
</intent-filter>
</receiver>
<activity
android:name=".activities.ControlCenterv2"
android:label="@string/title_activity_controlcenter"
@ -132,6 +150,10 @@
android:name=".activities.DashboardPreferencesActivity"
android:label="@string/dashboard_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.SleepAsAndroidPreferencesActivity"
android:label="@string/sleepasandroid_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.AboutUserPreferencesActivity"
android:label="@string/activity_prefs_about_you"
@ -859,4 +881,4 @@
android:parentActivityName=".activities.ControlCenterv2" />
</application>
</manifest>
</manifest>

View File

@ -106,7 +106,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -220,6 +220,12 @@ public class DebugActivity extends AbstractGBActivity {
replyAction.title = "Reply";
replyAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR;
notificationSpec.attachedActions.add(replyAction);
} else if (notificationSpec.type == NotificationType.CONVERSATIONS) {
// REPLY action
NotificationSpec.Action replyAction = new NotificationSpec.Action();
replyAction.title = "Reply";
replyAction.type = NotificationSpec.Action.TYPE_WEARABLE_REPLY;
notificationSpec.attachedActions.add(replyAction);
}
GBApplication.deviceService().onNotification(notificationSpec);
@ -659,7 +665,7 @@ public class DebugActivity extends AbstractGBActivity {
stopPhoneGpsLocationListener.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GBLocationManager.stopAll(getBaseContext());
GBLocationService.stop(DebugActivity.this, null);
}
});

View File

@ -421,6 +421,15 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
});
}
pref = findPreference("pref_category_sleepasandroid");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {
Intent enableIntent = new Intent(requireContext(), SleepAsAndroidPreferencesActivity.class);
startActivity(enableIntent);
return true;
});
}
final Preference theme = findPreference("pref_key_theme");
final Preference amoled_black = findPreference("pref_key_theme_amoled_black");

View File

@ -0,0 +1,138 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.devices.SleepAsAndroidFeature;
public class SleepAsAndroidPreferencesActivity extends AbstractSettingsActivityV2 {
@Override
protected String fragmentTag() {
return SleepAsAndroidPreferencesFragment.FRAGMENT_TAG;
}
@Override
protected PreferenceFragmentCompat newFragment() {
return new SleepAsAndroidPreferencesFragment();
}
public static class SleepAsAndroidPreferencesFragment extends AbstractPreferenceFragment {
static final String FRAGMENT_TAG = "SLEEPASANDROID_PREFERENCES_FRAGMENT";
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.sleepasandroid_preferences, rootKey);
final ListPreference sleepAsAndroidSlots = findPreference("sleepasandroid_alarm_slot");
if (sleepAsAndroidSlots != null)
{
loadAlarmSlots(sleepAsAndroidSlots);
}
final ListPreference sleepAsAndroidDevices = findPreference("sleepasandroid_device");
if (sleepAsAndroidDevices != null) {
loadDevicesList(sleepAsAndroidDevices);
sleepAsAndroidDevices.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
GBDevice device = GBApplication.app().getDeviceManager().getDeviceByAddress(newValue.toString());
if (device != null) {
GBApplication.getPrefs().getPreferences().edit().putString("sleepasandroid_device", device.getAddress()).apply();
Set<SleepAsAndroidFeature> supportedFeatures = device.getDeviceCoordinator().getSleepAsAndroidFeatures();
findPreference("sleepasandroid_alarm_slot").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
findPreference("pref_key_sleepasandroid_feat_alarms").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
findPreference("pref_key_sleepasandroid_feat_notifications").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.NOTIFICATIONS));
findPreference("pref_key_sleepasandroid_feat_movement").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ACCELEROMETER));
findPreference("pref_key_sleepasandroid_feat_hr").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.HEART_RATE));
findPreference("pref_key_sleepasandroid_feat_oximetry").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.OXIMETRY));
findPreference("pref_key_sleepasandroid_feat_spo2").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.SPO2));
ListPreference alarmSlots = findPreference("sleepasandroid_alarm_slot");
if (alarmSlots != null)
{
loadAlarmSlots(alarmSlots);
if (alarmSlots.getEntries().length > 0)
{
alarmSlots.setValueIndex(0);
GB.toast(getString(R.string.alarm_slot_reset), Toast.LENGTH_SHORT, GB.WARN);
}
}
}
return false;
}
});
}
String defaultDeviceAddr = GBApplication.getPrefs().getString("sleepasandroid_device", "");
if (!defaultDeviceAddr.isEmpty()) {
GBDevice device = GBApplication.app().getDeviceManager().getDeviceByAddress(defaultDeviceAddr);
if (device != null) {
Set<SleepAsAndroidFeature> supportedFeatures = device.getDeviceCoordinator().getSleepAsAndroidFeatures();
findPreference("sleepasandroid_alarm_slot").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
findPreference("pref_key_sleepasandroid_feat_alarms").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
findPreference("pref_key_sleepasandroid_feat_notifications").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.NOTIFICATIONS));
findPreference("pref_key_sleepasandroid_feat_movement").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ACCELEROMETER));
findPreference("pref_key_sleepasandroid_feat_hr").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.HEART_RATE));
findPreference("pref_key_sleepasandroid_feat_oximetry").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.OXIMETRY));
findPreference("pref_key_sleepasandroid_feat_spo2").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.SPO2));
}
}
}
}
private static void loadAlarmSlots(ListPreference sleepAsAndroidSlots) {
if (sleepAsAndroidSlots != null) {
String defaultDeviceAddr = GBApplication.getPrefs().getString("sleepasandroid_device", "");
if (!defaultDeviceAddr.isEmpty()) {
GBDevice device = GBApplication.app().getDeviceManager().getDeviceByAddress(defaultDeviceAddr);
if (device != null) {
int maxAlarmSlots = device.getDeviceCoordinator().getAlarmSlotCount(device);
if (maxAlarmSlots > 0) {
List<String> alarmSlots = new ArrayList<>();
int reservedAlarmSlots = GBApplication.getPrefs().getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_ALARMS_CALENDAR, 0);
for (int i = reservedAlarmSlots + 1;i < maxAlarmSlots; i++) {
alarmSlots.add(String.valueOf(i));
}
sleepAsAndroidSlots.setEntryValues(alarmSlots.toArray(new String[0]));
sleepAsAndroidSlots.setEntries(alarmSlots.toArray(new String[0]));
}
}
}
}
}
private static void loadDevicesList(ListPreference sleepAsAndroidDevices) {
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
List<String> deviceMACs = new ArrayList<>();
List<String> deviceNames = new ArrayList<>();
for (GBDevice dev : devices) {
if (dev.getDeviceCoordinator().supportsSleepAsAndroid()) {
deviceMACs.add(dev.getAddress());
deviceNames.add(dev.getAliasOrName());
}
}
sleepAsAndroidDevices.setEntryValues(deviceMACs.toArray(new String[0]));
sleepAsAndroidDevices.setEntries(deviceNames.toArray(new String[0]));
}
}

View File

@ -137,6 +137,9 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
}
private void draw() {
Prefs prefs = GBApplication.getPrefs();
boolean upsideDown24h = prefs.getBoolean("dashboard_widget_today_24h_upside_down", false);
// Prepare circular chart
long midDaySecond = dashboardData.timeFrom + (12 * 60 * 60);
int width = 500;
@ -183,7 +186,16 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
textPaint.setTextSize(hourTextPixels);
textPaint.setTextAlign(Paint.Align.CENTER);
Rect textBounds = new Rect();
if (mode_24h) {
if (mode_24h && upsideDown24h) {
textPaint.getTextBounds(hours.get(6), 0, hours.get(6).length(), textBounds);
canvas.drawText(hours.get(6), clockMargin + clockStripesWidth + textBounds.width() / 2f, height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(12), 0, hours.get(12).length(), textBounds);
canvas.drawText(hours.get(12), width / 2f, clockMargin + clockStripesWidth + textBounds.height(), textPaint);
textPaint.getTextBounds(hours.get(18), 0, hours.get(18).length(), textBounds);
canvas.drawText(hours.get(18), width - (clockMargin + clockStripesWidth + textBounds.width()), height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(24), 0, hours.get(24).length(), textBounds);
canvas.drawText(hours.get(24), width / 2f, height - (clockMargin + clockStripesWidth), textPaint);
} else if (mode_24h) {
textPaint.getTextBounds(hours.get(6), 0, hours.get(6).length(), textBounds);
canvas.drawText(hours.get(6), width - (clockMargin + clockStripesWidth + textBounds.width()), height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(12), 0, hours.get(12).length(), textBounds);
@ -216,6 +228,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
// Draw generalized activities on circular chart
long secondIndex = dashboardData.timeFrom;
long currentTime = Calendar.getInstance().getTimeInMillis() / 1000;
int startAngle = mode_24h && upsideDown24h ? 90 : 270;
synchronized (dashboardData.generalizedActivities) {
for (DashboardFragment.DashboardData.GeneralizedActivity activity : dashboardData.generalizedActivities) {
// Determine margin depending on 24h/12h mode
@ -224,15 +237,15 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
if (!mode_24h && secondIndex < midDaySecond && activity.timeFrom >= midDaySecond) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
secondIndex = midDaySecond;
}
if (activity.timeFrom > secondIndex) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(margin, margin, width - margin, height - margin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (activity.timeFrom - secondIndex) / degreeFactor, false, paint);
canvas.drawArc(margin, margin, width - margin, height - margin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (activity.timeFrom - secondIndex) / degreeFactor, false, paint);
}
float start_angle = 270 + (activity.timeFrom - dashboardData.timeFrom) / degreeFactor;
float start_angle = startAngle + (activity.timeFrom - dashboardData.timeFrom) / degreeFactor;
float sweep_angle = (activity.timeTo - activity.timeFrom) / degreeFactor;
if (activity.activityKind == ActivityKind.TYPE_NOT_MEASURED) {
paint.setStrokeWidth(barWidth / 3f);
@ -271,11 +284,11 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
// Fill inner bar up until current time
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
// Fill inner bar up until midday
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (currentTime - dashboardData.timeFrom) / degreeFactor, (midDaySecond - currentTime) / degreeFactor, false, paint);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (currentTime - dashboardData.timeFrom) / degreeFactor, (midDaySecond - currentTime) / degreeFactor, false, paint);
// Fill outer bar up until midnight
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
@ -287,24 +300,24 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
if (!mode_24h && secondIndex < midDaySecond) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
secondIndex = midDaySecond;
}
// Fill outer bar up until current time
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
// Fill outer bar up until midnight
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 270 + (currentTime - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - currentTime) / degreeFactor, false, paint);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, startAngle + (currentTime - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - currentTime) / degreeFactor, false, paint);
}
// Only when displaying a past day
if (secondIndex < dashboardData.timeTo && currentTime > dashboardData.timeTo) {
// Fill outer bar up until midnight
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - secondIndex) / degreeFactor, false, paint);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - secondIndex) / degreeFactor, false, paint);
}
todayChart.setImageBitmap(todayBitmap);

View File

@ -356,7 +356,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
});
}
addPreferenceHandlerFor(PREF_SEND_APP_NOTIFICATIONS);
addPreferenceHandlerFor(PREF_SWIPE_UNLOCK);
addPreferenceHandlerFor(PREF_MI2_DATEFORMAT);
addPreferenceHandlerFor(PREF_DATEFORMAT);

View File

@ -30,6 +30,7 @@ public enum DeviceSpecificSettingsScreen {
DEVELOPER("pref_screen_developer", R.xml.devicesettings_root_developer),
DISPLAY("pref_screen_display", R.xml.devicesettings_root_display),
GENERIC("pref_screen_generic", R.xml.devicesettings_root_generic),
LOCATION("pref_screen_location", R.xml.devicesettings_root_location),
NOTIFICATIONS("pref_screen_notifications", R.xml.devicesettings_root_notifications),
DATE_TIME("pref_screen_date_time", R.xml.devicesettings_root_date_time),
WORKOUT("pref_screen_workout", R.xml.devicesettings_root_workout),

View File

@ -843,7 +843,6 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
private void showDeviceSubmenu(final View v, final GBDevice device) {
boolean deviceConnected = device.getState() != GBDevice.State.NOT_CONNECTED;
PopupMenu menu = new PopupMenu(v.getContext(), v);
menu.inflate(R.menu.fragment_devices_device_submenu);

View File

@ -78,6 +78,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -586,6 +587,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false;
}
@Override
public boolean supportsSleepAsAndroid() {
return false;
}
@Override
public Set<SleepAsAndroidFeature> getSleepAsAndroidFeatures() {
return Collections.emptySet();
}
@Override
public int[] getSupportedDeviceSpecificConnectionSettings() {
int[] settings = new int[0];

View File

@ -29,6 +29,7 @@ import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
@ -57,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
/**
* This interface is implemented at least once for every supported gadget device.
@ -511,6 +513,11 @@ public interface DeviceCoordinator {
*/
boolean supportsMusicInfo();
/**
* Indicates whether the device supports features required by Sleep As Android
*/
boolean supportsSleepAsAndroid();
/**
* Indicates the maximum reminder message length.
*/
@ -569,6 +576,12 @@ public interface DeviceCoordinator {
*/
boolean supportsUnicodeEmojis();
/**
* Returns the set of supported sleep as Android features
* @return Set
*/
Set<SleepAsAndroidFeature> getSleepAsAndroidFeatures();
/**
* Returns device specific settings related to connection
*

View File

@ -20,6 +20,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.UUID;
@ -148,4 +149,6 @@ public interface EventHandler {
void onPowerOff();
void onSetGpsLocation(Location location);
void onSleepAsAndroidAction(String action, Bundle extras);
}

View File

@ -0,0 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
public enum SleepAsAndroidFeature {
HEART_RATE,
ALARMS,
NOTIFICATIONS,
ACCELEROMETER,
OXIMETRY,
SPO2
}

View File

@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
@ -14,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
@Override
@ -37,10 +39,8 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
notifications.add(R.xml.devicesettings_send_app_notifications);
final List<Integer> connection = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CONNECTION);
connection.add(R.xml.devicesettings_high_mtu);
notifications.add(R.xml.devicesettings_send_app_notifications);
if (getCannedRepliesSlotCount(device) > 0) {
notifications.add(R.xml.devicesettings_garmin_default_reply_suffix);
@ -48,6 +48,12 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
notifications.add(R.xml.devicesettings_canned_dismisscall_16);
}
final List<Integer> location = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.LOCATION);
location.add(R.xml.devicesettings_workout_send_gps_to_band);
final List<Integer> connection = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CONNECTION);
connection.add(R.xml.devicesettings_high_mtu);
final List<Integer> developer = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
developer.add(R.xml.devicesettings_keep_activity_data_on_device);
@ -68,4 +74,17 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
public boolean supportsWeather() {
return true;
}
@Override
public int getCannedRepliesSlotCount(final GBDevice device) {
if (getPrefs(device).getBoolean(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, false)) {
return 16;
}
return 0;
}
protected static Prefs getPrefs(final GBDevice device) {
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
}
}

View File

@ -2,4 +2,5 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
public class GarminPreferences {
public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities";
public static final String PREF_FEAT_CANNED_MESSAGES = "feat_canned_messages";
}

View File

@ -4,7 +4,6 @@ import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GarminInstinct2SCoordinator extends GarminCoordinator {
@Override
@ -12,11 +11,6 @@ public class GarminInstinct2SCoordinator extends GarminCoordinator {
return Pattern.compile("Instinct 2S");
}
@Override
public int getCannedRepliesSlotCount(final GBDevice device) {
return 16;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_instinct_2s;

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2solar;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import java.util.regex.Pattern;
public class GarminInstinct2SolarCoordinator extends GarminCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Instinct 2 Solar");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_instinct_2_solar;
}
}

View File

@ -27,6 +27,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -43,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabi
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiExtendedSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
@ -73,6 +76,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibration
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.devices.SleepAsAndroidFeature;
public abstract class ZeppOsCoordinator extends HuamiCoordinator {
public abstract String getDeviceBluetoothName();
@ -196,6 +200,16 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
return true;
}
@Override
public boolean supportsSleepAsAndroid() {
return true;
}
@Override
public Set<SleepAsAndroidFeature> getSleepAsAndroidFeatures() {
return EnumSet.of(SleepAsAndroidFeature.ACCELEROMETER, SleepAsAndroidFeature.HEART_RATE, SleepAsAndroidFeature.ALARMS, SleepAsAndroidFeature.NOTIFICATIONS);
}
@Override
public int getWorldClocksSlotCount() {
return 20; // as enforced by Zepp

View File

@ -30,6 +30,7 @@ import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
@ -83,6 +84,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.getSupportedLanguageSettings(device);
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_huawei_account};
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_ASK;

View File

@ -71,6 +71,7 @@ public final class HuaweiConstants {
public static final String PREF_HUAWEI_ADDRESS = "huawei_address";
public static final String PREF_HUAWEI_WORKMODE = "workmode";
public static final String PREF_HUAWEI_TRUSLEEP = "trusleep";
public static final String PREF_HUAWEI_ACCOUNT = "huawei_account";
public static final String PREF_HUAWEI_DND_LIFT_WRIST_TYPE = "dnd_lift_wrist_type"; // SharedPref for 0x01 0x1D
public static final String PREF_HUAWEI_DEBUG_REQUEST = "debug_huawei_request";

View File

@ -30,6 +30,7 @@ import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
@ -82,7 +83,12 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
public String[] getSupportedLanguageSettings(GBDevice device) {
return huaweiCoordinator.getSupportedLanguageSettings(device);
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_huawei_account};
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_NONE;

View File

@ -26,15 +26,18 @@ public class AccountRelated {
public static final byte id = 0x01;
public static class Request extends HuaweiPacket {
public Request (ParamsProvider paramsProvider) {
public Request (ParamsProvider paramsProvider, String account) {
super(paramsProvider);
this.serviceId = AccountRelated.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01);
this.tlv = new HuaweiTLV();
if (account.length() > 0) {
tlv.put(0x01, account);
} else {
tlv.put(0x01);
}
this.complete = true;
}
}
@ -50,14 +53,19 @@ public class AccountRelated {
public static final byte id = 0x05;
public static class Request extends HuaweiPacket {
public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization) {
public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization, String account) {
super(paramsProvider);
this.serviceId = AccountRelated.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, (byte)0x00);
this.tlv = new HuaweiTLV();
if (account.length() > 0) {
tlv.put(0x01, account);
} else {
tlv.put(0x01, (byte)0x00);
}
if (accountPairingOptimization) {
this.tlv.put(0x03, (byte)0x01);
}

View File

@ -26,6 +26,7 @@ import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.GregorianCalendar;
@ -188,6 +189,7 @@ public class CalendarReceiver extends BroadcastReceiver {
calendarEventSpec.id = i;
calendarEventSpec.title = calendarEvent.getTitle();
calendarEventSpec.allDay = calendarEvent.isAllDay();
calendarEventSpec.reminders = new ArrayList<>(calendarEvent.getRemindersAbsoluteTs());
calendarEventSpec.timestamp = calendarEvent.getBeginSeconds();
calendarEventSpec.durationInSeconds = calendarEvent.getDurationSeconds(); //FIXME: leads to problems right now
if (calendarEvent.isAllDay()) {

View File

@ -21,10 +21,14 @@ import android.location.LocationListener;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/**
* An implementation of a {@link LocationListener} that forwards the location updates to the
@ -33,18 +37,18 @@ import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
public class GBLocationListener implements LocationListener {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationListener.class);
private final EventHandler eventHandler;
private final GBDevice device;
private Location previousLocation;
// divide by 3.6 to get km/h to m/s
private static final double SPEED_THRESHOLD = 1.0 / 3.6;
public GBLocationListener(final EventHandler eventHandler) {
this.eventHandler = eventHandler;
public GBLocationListener(final GBDevice device) {
this.device = device;
}
@Override
public void onLocationChanged(final Location location) {
public void onLocationChanged(@NonNull final Location location) {
LOG.info("Location changed: {}", location);
// Correct the location time
@ -61,16 +65,16 @@ public class GBLocationListener implements LocationListener {
previousLocation = location;
eventHandler.onSetGpsLocation(location);
GBApplication.deviceService(device).onSetGpsLocation(location);
}
@Override
public void onProviderDisabled(final String provider) {
public void onProviderDisabled(@NonNull final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}
@Override
public void onProviderEnabled(final String provider) {
public void onProviderEnabled(@NonNull final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}

View File

@ -1,140 +0,0 @@
/* Copyright (C) 2022-2024 halemmerich, José Rebelo, LukasEdl, Martin Boonk
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.HashSet;
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 running.
*/
public class GBLocationManager {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationManager.class);
/**
* The current number of running listeners.
*/
private static Map<EventHandler, Map<LocationProviderType, AbstractLocationProvider>> providers = new HashMap<>();
public static void start(final Context context, final EventHandler eventHandler) {
GBLocationManager.start(context, eventHandler, LocationProviderType.GPS, null);
}
public static void start(final Context context, final EventHandler eventHandler, final LocationProviderType providerType, Integer updateInterval) {
LOG.info("Starting");
if (providers.containsKey(eventHandler) && providers.get(eventHandler).containsKey(providerType)) {
LOG.warn("EventHandler already registered");
return;
}
GB.createGpsNotification(context, providers.size());
final GBLocationListener locationListener = new GBLocationListener(eventHandler);
final AbstractLocationProvider locationProvider;
switch (providerType) {
case GPS:
LOG.info("Using gps location provider");
locationProvider = new PhoneGpsLocationProvider(locationListener);
break;
case NETWORK:
LOG.info("Using network location provider");
locationProvider = new PhoneNetworkLocationProvider(locationListener);
break;
default:
LOG.info("Using default location provider: GPS");
locationProvider = new PhoneGpsLocationProvider(locationListener);
}
if (updateInterval != null) {
locationProvider.start(context, updateInterval);
} else {
locationProvider.start(context);
}
if (providers.containsKey(eventHandler)) {
providers.get(eventHandler).put(providerType, locationProvider);
} else {
Map<LocationProviderType, AbstractLocationProvider> providerMap = new HashMap<>();
providerMap.put(providerType, locationProvider);
providers.put(eventHandler, providerMap);
}
}
public static void stop(final Context context, final EventHandler eventHandler) {
GBLocationManager.stop(context, eventHandler, null);
}
public static void stop(final Context context, final EventHandler eventHandler, final LocationProviderType gpsType) {
if (!providers.containsKey(eventHandler)) return;
Map<LocationProviderType, AbstractLocationProvider> providerMap = providers.get(eventHandler);
if (gpsType == null) {
Set<LocationProviderType> toBeRemoved = new HashSet<>();
for (LocationProviderType providerType: providerMap.keySet()) {
stopProvider(context, providerMap.get(providerType));
toBeRemoved.add(providerType);
}
for (final LocationProviderType providerType : toBeRemoved) {
providerMap.remove(providerType);
}
} else {
stopProvider(context, providerMap.get(gpsType));
providerMap.remove(gpsType);
}
LOG.debug("Remaining providers: " + providers.size());
if (providers.get(eventHandler).size() == 0)
providers.remove(eventHandler);
updateNotification(context);
}
private static void updateNotification(final Context context){
if (!providers.isEmpty()) {
GB.createGpsNotification(context, providers.size());
} else {
GB.removeGpsNotification(context);
}
}
private static void stopProvider(final Context context, AbstractLocationProvider locationProvider) {
if (locationProvider != null) {
locationProvider.stop(context);
}
}
public static void stopAll(final Context context) {
for (EventHandler eventHandler : providers.keySet()) {
stop(context, eventHandler);
}
}
}

View File

@ -22,35 +22,30 @@ import android.location.LocationListener;
/**
* An abstract location provider, which periodically sends a location update to the provided {@link LocationListener}.
*/
public abstract class AbstractLocationProvider {
public abstract class GBLocationProvider {
private final Context context;
private final LocationListener locationListener;
public AbstractLocationProvider(final LocationListener locationListener) {
public GBLocationProvider(final Context context, final LocationListener locationListener) {
this.context = context;
this.locationListener = locationListener;
}
protected final LocationListener getLocationListener() {
public final Context getContext() {
return this.context;
}
public final LocationListener getLocationListener() {
return this.locationListener;
}
/**
* Start sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void start(final Context context);
/**
* Start sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void start(final Context context, final int interval);
public abstract void start(final int interval);
/**
* Stop sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void stop(final Context context);
public abstract void stop();
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2022-2024 LukasEdl
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.content.Context;
import android.location.LocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers.MockLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers.PhoneLocationProvider;
public enum GBLocationProviderType {
GPS {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new PhoneLocationProvider(context, locationListener, LocationManager.GPS_PROVIDER);
}
},
NETWORK {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new PhoneLocationProvider(context, locationListener, LocationManager.NETWORK_PROVIDER);
}
},
MOCK {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new MockLocationProvider(context, locationListener);
}
},
;
public abstract GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener);
}

View File

@ -0,0 +1,184 @@
/* Copyright (C) 2022-2024 halemmerich, José Rebelo, LukasEdl, Martin Boonk
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils;
/**
* A static location manager, which keeps track of what providers are currently running. A notification is kept
* while there is at least one provider running.
*/
public class GBLocationService extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationService.class);
public static final String ACTION_START = "GBLocationService.START";
public static final String ACTION_STOP = "GBLocationService.STOP";
public static final String ACTION_STOP_ALL = "GBLocationService.STOP_ALL";
public static final String EXTRA_TYPE = "extra_type";
public static final String EXTRA_INTERVAL = "extra_interval";
private final Context context;
private final Map<GBDevice, List<GBLocationProvider>> providersByDevice = new HashMap<>();
public GBLocationService(final Context context) {
this.context = context;
}
@Override
public void onReceive(final Context context, final Intent intent) {
if (intent.getAction() == null) {
LOG.warn("Action is null");
return;
}
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
switch (intent.getAction()) {
case ACTION_START:
if (device == null) {
LOG.error("Device is null for {}", intent.getAction());
return;
}
final GBLocationProviderType providerType = GBLocationProviderType.valueOf(
intent.hasExtra(EXTRA_TYPE) ? intent.getStringExtra(EXTRA_TYPE) : "GPS"
);
final int updateInterval = intent.getIntExtra(EXTRA_INTERVAL, 1000);
LOG.debug("Starting location provider {} for {}", providerType, device.getAliasOrName());
if (!providersByDevice.containsKey(device)) {
providersByDevice.put(device, new ArrayList<>());
}
updateNotification();
final List<GBLocationProvider> existingProviders = providersByDevice.get(device);
final GBLocationListener locationListener = new GBLocationListener(device);
final GBLocationProvider locationProvider = providerType.newInstance(context, locationListener);
locationProvider.start(updateInterval);
Objects.requireNonNull(existingProviders).add(locationProvider);
return;
case ACTION_STOP:
if (device != null) {
stopDevice(device);
updateNotification();
} else {
stopAll();
}
return;
case ACTION_STOP_ALL:
stopAll();
return;
default:
LOG.warn("Unknown action {}", intent.getAction());
}
}
public void stopDevice(final GBDevice device) {
LOG.debug("Stopping location providers for {}", device.getAliasOrName());
final List<GBLocationProvider> providers = providersByDevice.remove(device);
if (providers != null) {
for (final GBLocationProvider provider : providers) {
provider.stop();
}
}
}
public IntentFilter buildFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_START);
intentFilter.addAction(ACTION_STOP);
return intentFilter;
}
public void stopAll() {
LOG.info("Stopping location service for all devices");
final List<GBDevice> gbDevices = new ArrayList<>(providersByDevice.keySet());
for (GBDevice d : gbDevices) {
stopDevice(d);
}
updateNotification();
}
public static void start(final Context context,
@NonNull final GBDevice device,
final GBLocationProviderType providerType,
final int updateInterval) {
final Intent intent = new Intent(ACTION_START);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
intent.putExtra(EXTRA_TYPE, providerType.name());
intent.putExtra(EXTRA_INTERVAL, updateInterval);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public static void stop(final Context context, @Nullable final GBDevice device) {
final Intent intent = new Intent(ACTION_STOP);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
private void updateNotification() {
if (!providersByDevice.isEmpty()) {
final Intent notificationIntent = new Intent(context, GBLocationService.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
final PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false);
final NotificationCompat.Builder nb = new NotificationCompat.Builder(context, GB.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, providersByDevice.size()))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_gps_location)
.setOngoing(true);
GB.notify(GB.NOTIFICATION_ID_GPS, nb.build(), context);
} else {
GB.removeNotification(GB.NOTIFICATION_ID_GPS, context);
}
}
}

View File

@ -1,22 +0,0 @@
/* Copyright (C) 2022-2024 LukasEdl
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
public enum LocationProviderType {
GPS,
NETWORK,
}

View File

@ -1,80 +0,0 @@
/* Copyright (C) 2022-2024 Lukas, LukasEdl
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
import android.Manifest;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Looper;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A location provider that uses the phone GPS, using {@link LocationManager}.
*/
public class PhoneNetworkLocationProvider extends AbstractLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneNetworkLocationProvider.class);
private static final int INTERVAL_MIN_TIME = 1000;
private static final int INTERVAL_MIN_DISTANCE = 0;
public PhoneNetworkLocationProvider(LocationListener locationListener) {
super(locationListener);
}
@Override
void start(final Context context) {
start(context, INTERVAL_MIN_TIME);
}
@Override
void start(Context context, int interval) {
LOG.info("Starting phone network location provider");
if (!GB.checkPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)) {
GB.toast("Location permission not granted", Toast.LENGTH_SHORT, GB.ERROR);
return;
}
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
interval,
INTERVAL_MIN_DISTANCE,
getLocationListener(),
Looper.getMainLooper()
);
final Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
LOG.debug("Last known network location: {}", lastKnownLocation);
}
@Override
void stop(final Context context) {
LOG.info("Stopping phone network location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
}
}

View File

@ -14,7 +14,7 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers;
import android.content.Context;
import android.location.Location;
@ -26,13 +26,14 @@ import android.os.SystemClock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProvider;
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 {
public class MockLocationProvider extends GBLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(MockLocationProvider.class);
private Location previousLocation = new CurrentPosition().getLastKnownLocation();
@ -40,12 +41,12 @@ public class MockLocationProvider extends AbstractLocationProvider {
/**
* Interval between location updates, in milliseconds.
*/
private final int interval = 1000;
private static final int DEFAULT_INTERVAL = 1000;
/**
* Difference between location updates, in degrees.
*/
private final float coordDiff = 0.0002f;
private static final float COORD_DIFF = 0.0002f;
/**
* Whether the handler is running.
@ -54,50 +55,40 @@ public class MockLocationProvider extends AbstractLocationProvider {
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());
newLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
getLocationListener().onLocationChanged(newLocation);
previousLocation = newLocation;
if (running) {
handler.postDelayed(this, interval);
}
}
};
public MockLocationProvider(LocationListener locationListener) {
super(locationListener);
public MockLocationProvider(final Context context, final LocationListener locationListener) {
super(context, locationListener);
}
@Override
void start(final Context context) {
public void start(final int interval) {
LOG.info("Starting mock location provider");
running = true;
handler.postDelayed(locationUpdateRunnable, interval);
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (!running) {
return;
}
final Location newLocation = new Location(previousLocation);
newLocation.setLatitude(previousLocation.getLatitude() + COORD_DIFF);
newLocation.setTime(System.currentTimeMillis());
newLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
getLocationListener().onLocationChanged(newLocation);
previousLocation = newLocation;
if (running) {
handler.postDelayed(this, interval);
}
}
}, interval > 0 ? interval : DEFAULT_INTERVAL);
}
@Override
void start(final Context context, int minInterval) {
LOG.info("Starting mock location provider");
running = true;
handler.postDelayed(locationUpdateRunnable, interval);
}
@Override
void stop(final Context context) {
public void stop() {
LOG.info("Stopping mock location provider");
running = false;

View File

@ -14,7 +14,7 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps;
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers;
import android.Manifest;
import android.content.Context;
@ -27,43 +27,38 @@ import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* 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);
public class PhoneLocationProvider extends GBLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneLocationProvider.class);
private final String provider;
private static final int INTERVAL_MIN_TIME = 1000;
private static final int INTERVAL_MIN_DISTANCE = 0;
public PhoneGpsLocationProvider(LocationListener locationListener) {
super(locationListener);
}
public PhoneGpsLocationProvider(LocationListener locationListener, int intervalTime) {
super(locationListener);
public PhoneLocationProvider(final Context context, final LocationListener locationListener, final String provider) {
super(context, locationListener);
this.provider = provider;
}
@Override
void start(final Context context) {
start(context, INTERVAL_MIN_TIME);
}
@Override
void start(Context context, int interval) {
public void start(final int interval) {
LOG.info("Starting phone gps location provider");
if (!GB.checkPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)) {
if (!GB.checkPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION)) {
GB.toast("Location permission not granted", Toast.LENGTH_SHORT, GB.ERROR);
return;
}
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
final LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
interval,
provider,
interval > 0 ? interval : 1_000,
INTERVAL_MIN_DISTANCE,
getLocationListener(),
Looper.getMainLooper()
@ -74,10 +69,10 @@ public class PhoneGpsLocationProvider extends AbstractLocationProvider {
}
@Override
void stop(final Context context) {
public void stop() {
LOG.info("Stopping phone gps location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
final LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
}
}

View File

@ -0,0 +1,16 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid;
public class SleepAsAndroidAction {
public static final String START_TRACKING = "com.urbandroid.sleep.watch.START_TRACKING";
public static final String STOP_TRACKING = "com.urbandroid.sleep.watch.STOP_TRACKING";
public static final String SET_PAUSE = "com.urbandroid.sleep.watch.SET_PAUSE";
public static final String SET_SUSPENDED = "com.urbandroid.sleep.watch.SET_SUSPENDED";
public static final String SET_BATCH_SIZE = "com.urbandroid.sleep.watch.SET_BATCH_SIZE";
public static final String START_ALARM = "com.urbandroid.sleep.watch.START_ALARM";
public static final String STOP_ALARM = "com.urbandroid.sleep.watch.STOP_ALARM";
public static final String UPDATE_ALARM = "com.urbandroid.sleep.watch.UPDATE_ALARM";
public static final String SHOW_NOTIFICATION = "com.urbandroid.sleep.watch.SHOW_NOTIFICATION";
public static final String HINT = "com.urbandroid.sleep.watch.HINT";
public static final String CHECK_CONNECTED = "com.urbandroid.sleep.watch.CHECK_CONNECTED";
public static final String CONFIRM_CONNECTED = "com.urbandroid.sleep.watch.CONFIRM_CONNECTED";
}

View File

@ -0,0 +1,23 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
public class SleepAsAndroidReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(SleepAsAndroidReceiver.class);
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_enable", false)) {
GBApplication.deviceService().onSleepAsAndroidAction(action, intent.getExtras());
}
}
}

View File

@ -26,7 +26,7 @@ import android.content.Intent;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.os.Parcelable;
import android.os.Bundle;
import android.provider.ContactsContract;
import java.util.ArrayList;
@ -451,6 +451,7 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_CALENDAREVENT_TIMESTAMP, calendarEventSpec.timestamp)
.putExtra(EXTRA_CALENDAREVENT_DURATION, calendarEventSpec.durationInSeconds)
.putExtra(EXTRA_CALENDAREVENT_ALLDAY, calendarEventSpec.allDay)
.putExtra(EXTRA_CALENDAREVENT_REMINDERS, calendarEventSpec.reminders)
.putExtra(EXTRA_CALENDAREVENT_TITLE, calendarEventSpec.title)
.putExtra(EXTRA_CALENDAREVENT_DESCRIPTION, calendarEventSpec.description)
.putExtra(EXTRA_CALENDAREVENT_CALNAME, calendarEventSpec.calName)
@ -547,4 +548,14 @@ public class GBDeviceService implements DeviceService {
intent.putExtra(EXTRA_GPS_LOCATION, location);
invokeService(intent);
}
@Override
public void onSleepAsAndroidAction(String action, Bundle extras) {
Intent intent = createIntent().setAction(ACTION_SLEEP_AS_ANDROID);
intent.putExtra(EXTRA_SLEEP_AS_ANDROID_ACTION, action);
if (extras != null) {
intent.putExtras(extras);
}
invokeService(intent);
}
}

View File

@ -17,6 +17,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import java.util.ArrayList;
public class CalendarEventSpec {
public static final byte TYPE_UNKNOWN = 0;
public static final byte TYPE_SUNRISE = 1;
@ -32,4 +34,5 @@ public class CalendarEventSpec {
public String calName;
public int color;
public boolean allDay;
public ArrayList<Long> reminders; // unix epoch millis
}

View File

@ -79,6 +79,9 @@ public interface DeviceService extends EventHandler {
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 ACTION_SLEEP_AS_ANDROID = ".action.sleep_as_android";
String EXTRA_SLEEP_AS_ANDROID_ACTION = "sleepasandroid_action";
String EXTRA_NOTIFICATION_BODY = "notification_body";
String EXTRA_NOTIFICATION_FLAGS = "notification_flags";
String EXTRA_NOTIFICATION_ID = "notification_id";
@ -159,6 +162,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_CALENDAREVENT_TIMESTAMP = "calendarevent_timestamp";
String EXTRA_CALENDAREVENT_DURATION = "calendarevent_duration";
String EXTRA_CALENDAREVENT_ALLDAY = "calendarevent_allday";
String EXTRA_CALENDAREVENT_REMINDERS = "calendarevent_reminders";
String EXTRA_CALENDAREVENT_TITLE = "calendarevent_title";
String EXTRA_CALENDAREVENT_DESCRIPTION = "calendarevent_description";
String EXTRA_CALENDAREVENT_LOCATION = "calendarevent_location";

View File

@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsLiveDe
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsProDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.forerunner245.GarminForerunner245Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2s.GarminInstinct2SCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2solar.GarminInstinct2SolarCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.venu3.GarminVenu3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive4s.GarminVivoActive4SCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive5.GarminVivoActive5Coordinator;
@ -329,6 +330,7 @@ public enum DeviceType {
VIVOMOVE_HR(VivomoveHrCoordinator.class),
GARMIN_FORERUNNER_245(GarminForerunner245Coordinator.class),
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
GARMIN_INSTINCT_2_SOLAR(GarminInstinct2SolarCoordinator.class),
GARMIN_VIVOMOVE_STYLE(GarminVivomoveStyleCoordinator.class),
GARMIN_VENU_3(GarminVenu3Coordinator.class),
GARMIN_VIVOACTIVE_4S(GarminVivoActive4SCoordinator.class),

View File

@ -32,6 +32,7 @@ import android.graphics.BitmapFactory;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.text.TextUtils;
@ -1181,4 +1182,9 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) {
}
@Override
public void onSleepAsAndroidAction(String action, Bundle extras) {
}
}

View File

@ -82,6 +82,8 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.SilentModeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGermanyReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidReceiver;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
@ -138,13 +140,15 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
}
private class FeatureSet{
private static class FeatureSet {
private boolean supportsWeather = false;
private boolean supportsActivityDataFetching = false;
private boolean supportsCalendarEvents = false;
private boolean supportsMusicInfo = false;
private boolean supportsNavigation = false;
private boolean supportsSleepAsAndroid = false;
public boolean supportsWeather() {
return supportsWeather;
}
@ -185,6 +189,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
this.supportsNavigation = supportsNavigation;
}
public boolean supportsSleepAsAndroid() { return supportsSleepAsAndroid; }
public void setSupportsSleepAsAndroid(boolean supportsSleepAsAndroid) {
this.supportsSleepAsAndroid = supportsSleepAsAndroid;
}
public void logicalOr(DeviceCoordinator operand){
if(operand.supportsCalendarEvents()){
setSupportsCalendarEvents(true);
@ -201,6 +211,9 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
if(operand.supportsNavigation()){
setSupportsNavigation(true);
}
if (operand.supportsSleepAsAndroid()) {
setSupportsSleepAsAndroid(true);
}
}
}
@ -243,7 +256,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private AutoConnectIntervalReceiver mAutoConnectInvervalReceiver = null;
private AlarmReceiver mAlarmReceiver = null;
private List<CalendarReceiver> mCalendarReceiver = new ArrayList<>();
private final List<CalendarReceiver> mCalendarReceiver = new ArrayList<>();
private CMWeatherReceiver mCMWeatherReceiver = null;
private LineageOsWeatherReceiver mLineageOsWeatherReceiver = null;
private TinyWeatherForecastGermanyReceiver mTinyWeatherForecastGermanyReceiver = null;
@ -251,9 +264,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private OmniJawsObserver mOmniJawsObserver = null;
private final DeviceSettingsReceiver deviceSettingsReceiver = new DeviceSettingsReceiver();
private final IntentApiReceiver intentApiReceiver = new IntentApiReceiver();
private GBLocationService locationService = null;
private OsmandEventReceiver mOsmandAidlHelper = null;
private SleepAsAndroidReceiver mSleepAsAndroidReceiver = null;
private HashMap<String, Long> deviceLastScannedTimestamps = new HashMap<>();
private final String[] mMusicActions = {
@ -845,6 +861,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
calendarEventSpec.timestamp = intent.getIntExtra(EXTRA_CALENDAREVENT_TIMESTAMP, -1);
calendarEventSpec.durationInSeconds = intent.getIntExtra(EXTRA_CALENDAREVENT_DURATION, -1);
calendarEventSpec.allDay = intent.getBooleanExtra(EXTRA_CALENDAREVENT_ALLDAY, false);
calendarEventSpec.reminders = (ArrayList<Long>) intent.getSerializableExtra(EXTRA_CALENDAREVENT_REMINDERS);
calendarEventSpec.title = intent.getStringExtra(EXTRA_CALENDAREVENT_TITLE);
calendarEventSpec.description = intent.getStringExtra(EXTRA_CALENDAREVENT_DESCRIPTION);
calendarEventSpec.location = intent.getStringExtra(EXTRA_CALENDAREVENT_LOCATION);
@ -1069,6 +1086,13 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
final Location location = intent.getParcelableExtra(EXTRA_GPS_LOCATION);
deviceSupport.onSetGpsLocation(location);
break;
case ACTION_SLEEP_AS_ANDROID:
if(device.getDeviceCoordinator().supportsSleepAsAndroid() && GBApplication.getPrefs().getString("sleepasandroid_device", new String()).equals(device.getAddress()))
{
final String sleepAsAndroidAction = intent.getStringExtra(EXTRA_SLEEP_AS_ANDROID_ACTION);
deviceSupport.onSleepAsAndroidAction(sleepAsAndroidAction, intent.getExtras());
}
break;
}
}
@ -1320,6 +1344,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
registerReceiver(mSilentModeReceiver, filter);
}
if (locationService == null) {
locationService = new GBLocationService(this);
LocalBroadcastManager.getInstance(this).registerReceiver(locationService, locationService.buildFilter());
}
if (mOsmandAidlHelper == null && features.supportsNavigation()) {
mOsmandAidlHelper = new OsmandEventReceiver(this.getApplication());
}
@ -1355,6 +1384,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
}
if (features.supportsSleepAsAndroid())
{
if (mSleepAsAndroidReceiver == null) {
mSleepAsAndroidReceiver = new SleepAsAndroidReceiver();
registerReceiver(mSleepAsAndroidReceiver, new IntentFilter());
}
}
if (GBApplication.getPrefs().getBoolean("auto_fetch_enabled", false) &&
features.supportsActivityDataFetching() && mGBAutoFetchReceiver == null) {
mGBAutoFetchReceiver = new GBAutoFetchReceiver();
@ -1394,6 +1431,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mSilentModeReceiver);
mSilentModeReceiver = null;
}
if (locationService != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(locationService);
locationService.stopAll();
locationService = null;
}
if (mCMWeatherReceiver != null) {
unregisterReceiver(mCMWeatherReceiver);
mCMWeatherReceiver = null;
@ -1422,6 +1464,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mGenericWeatherReceiver);
mGenericWeatherReceiver = null;
}
if (mSleepAsAndroidReceiver != null) {
unregisterReceiver(mSleepAsAndroidReceiver);
mSleepAsAndroidReceiver = null;
}
}
}

View File

@ -22,12 +22,12 @@ import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.UUID;
@ -512,4 +512,12 @@ public class ServiceDeviceSupport implements DeviceSupport {
}
delegate.onSetGpsLocation(location);
}
@Override
public void onSleepAsAndroidAction(String action, Bundle extras) {
if (checkBusy("sleep as android")) {
return;
}
delegate.onSleepAsAndroidAction(action, extras);
}
}

View File

@ -0,0 +1,573 @@
package nodomain.freeyourgadget.gadgetbridge.service;
import android.content.Context;
import android.content.Intent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.SleepAsAndroidFeature;
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidAction;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class SleepAsAndroidSender {
private final Logger LOG = LoggerFactory.getLogger(SleepAsAndroidSender.class);
private final String PACKAGE_SLEEP_AS_ANDROID = "com.urbandroid.sleep";
private final String ACTION_EXTRA_DATA_UPDATE = "com.urbandroid.sleep.ACTION_EXTRA_DATA_UPDATE";
private final String ACTION_MOVEMENT_DATA_UPDATE = "com.urbandroid.sleep.watch.DATA_UPDATE";
private final String ACTION_HEART_RATE_DATA_UPDATE = "com.urbandroid.sleep.watch.HR_DATA_UPDATE";
private final String ACTION_RESUME_FROM_WATCH = "com.urbandroid.sleep.watch.RESUME_FROM_WATCH";
private final String ACTION_PAUSE_FROM_WATCH = "com.urbandroid.sleep.watch.PAUSE_FROM_WATCH";
private final String ACTION_SNOOZE_FROM_WATCH = "com.urbandroid.sleep.watch.SNOOZE_FROM_WATCH";
private final String ACTION_DISMISS_FROM_WATCH = "com.urbandroid.sleep.watch.DISMISS_FROM_WATCH";
private final String MAX_RAW_DATA = "MAX_RAW_DATA";
private final String DATA = "DATA";
private final String EXTRA_DATA_HR = "com.urbandroid.sleep.EXTRA_DATA_HR";
private final String EXTRA_DATA_RR = "com.urbandroid.sleep.EXTRA_DATA_RR";
private final String EXTRA_DATA_SPO2 = "com.urbandroid.sleep.EXTRA_DATA_SPO2";
private final String EXTRA_DATA_SDNN = "com.urbandroid.sleep.EXTRA_DATA_SDNN";
private final String EXTRA_DATA_TIMESTAMP = "com.urbandroid.sleep.EXTRA_DATA_TIMESTAMP";
private final String EXTRA_DATA_FRAMERATE = "com.urbandroid.sleep.EXTRA_DATA_FRAMERATE";
private final String EXTRA_DATA_BATCH = "com.urbandroid.sleep.EXTRA_DATA_BATCH";
private GBDevice device;
private boolean trackingOngoing = false;
private boolean trackingPaused = false;
private ScheduledExecutorService trackingPauseScheduler;
private long batchSize = 1;
private long lastRawDataMs = 0;
private float maxRawData = 0;
private long lastHrDataMs = 0;
private ArrayList<Float> hrData = new ArrayList<>();
private ArrayList<Float> accData = new ArrayList<>();
private ScheduledExecutorService accDataScheduler;
private Set<SleepAsAndroidFeature> features;
public SleepAsAndroidSender(GBDevice gbDevice) {
this.device = gbDevice;
this.features = gbDevice.getDeviceCoordinator().getSleepAsAndroidFeatures();
}
/**
* Check if a SleepAsAndroid feature is enabled
*
* @param feature the feature
* @return true if the feature is enabled
*/
public boolean hasFeature(SleepAsAndroidFeature feature) {
return this.features.contains(feature);
}
/**
* Check if a SleepAsAndroid feature is enabled
* @param feature
* @return
*/
public boolean isFeatureEnabled(SleepAsAndroidFeature feature) {
boolean enabled = isSleepAsAndroidEnabled();
if (enabled) {
switch (feature) {
case ACCELEROMETER:
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_movement", false);
break;
case HEART_RATE:
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_hr", false);
break;
case SPO2:
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_spo2", false);
break;
case OXIMETRY:
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_oximetry", false);
break;
case NOTIFICATIONS:
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_notifications", false);
break;
case ALARMS:
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_alarms", false);
break;
default:
break;
}
}
return enabled;
}
/**
* Get all enabled features
*
* @return all enabled features
*/
public Set<SleepAsAndroidFeature> getFeatures() {
return features;
}
/**
* Start tracking
*/
public void startTracking() {
if (!isDeviceDefault()) return;
stopTracking();
accDataScheduler = Executors.newSingleThreadScheduledExecutor();
accDataScheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
aggregateAndSendAccelData();
}
}, 9999, 9999, TimeUnit.MILLISECONDS);
lastRawDataMs = System.currentTimeMillis();
lastHrDataMs = System.currentTimeMillis();
this.trackingOngoing = true;
}
/**
* Stop tracking
*/
public void stopTracking() {
if (!isDeviceDefault() || !trackingOngoing) return;
if (accDataScheduler != null) {
accDataScheduler.shutdownNow();
accDataScheduler = null;
}
this.trackingOngoing = false;
this.hrData = new ArrayList<>();
this.accData = new ArrayList<>();
this.lastHrDataMs = 0;
this.lastRawDataMs = 0;
}
/**
* Pause tracking
*
* @param timeout the timeout in milliseconds before resuming
*/
public void pauseTracking(long timeout) {
if (!isDeviceDefault() || !trackingOngoing) return;
if (timeout <= 0) {
resumeTracking();
return;
}
pauseTracking();
trackingPauseScheduler = setPauseTracking(timeout);
}
/**
* Same as {@link #pauseTracking(long)} but pausing and resuming is controlled by a toggle
*
* @param suspended true if the tracking should be paused, false if should be resumed
*/
public void pauseTracking(boolean suspended) {
if (!isDeviceDefault() || !trackingOngoing) return;
trackingPaused = suspended;
if (!trackingPaused) {
resumeTracking();
return;
}
}
/**
* Set a scheduler to resume tracking
*
* @param delay the delay
* @return the scheduler
*/
private ScheduledExecutorService setPauseTracking(long delay) {
trackingPaused = true;
trackingPauseScheduler = Executors.newSingleThreadScheduledExecutor();
trackingPauseScheduler.schedule(new Runnable() {
@Override
public void run() {
resumeTracking();
}
}, delay, TimeUnit.MILLISECONDS);
return trackingPauseScheduler;
}
/**
* Pause tracking
*/
private void pauseTracking() {
if (!isDeviceDefault() && trackingPaused) return;
trackingPaused = true;
stopTracking();
}
/**
* Resume tracking
*/
private void resumeTracking() {
if (!isDeviceDefault() && !trackingPaused) return;
if (trackingPauseScheduler != null) {
trackingPauseScheduler.shutdownNow();
trackingPauseScheduler = null;
}
trackingPaused = false;
startTracking();
}
/**
* Set the batch size
*
* @param batchSize the batch size
*/
public void setBatchSize(long batchSize) {
if (!isDeviceDefault()) return;
LOG.debug("Setting batch size to " + batchSize);
this.batchSize = batchSize;
}
/**
* Confirm that the device is connected
*/
public void confirmConnected() {
if (!isDeviceDefault()) return;
LOG.debug("Confirming connected");
Intent intent = new Intent(SleepAsAndroidAction.CONFIRM_CONNECTED);
broadcastToSleepAsAndroid(intent);
}
/**
* On accelerometer changed
*
* @param x the x value
* @param y the y value
* @param z the z value
*/
public void onAccelChanged(float x, float y, float z) {
if (!isDeviceDefault() || !isFeatureEnabled(SleepAsAndroidFeature.ACCELEROMETER) || !hasFeature(SleepAsAndroidFeature.ACCELEROMETER) || !trackingOngoing)
return;
if (trackingPaused)
return;
updateMaxRawData(x, y, z);
}
/**
* Aggregate and send the acceleration data
*/
private synchronized void aggregateAndSendAccelData() {
if (!trackingOngoing || trackingPaused) return;
if (maxRawData > 0) {
accData.add(maxRawData);
maxRawData = 0;
if (accData.size() == batchSize) {
sendAccelData();
}
}
}
/**
* Send the acceleration data
*/
private void sendAccelData() {
LOG.debug("Sending movement data: " + this.accData + " batch size: " + batchSize + " array size: " + accData.size());
Intent intent = new Intent(ACTION_MOVEMENT_DATA_UPDATE);
intent.putExtra(MAX_RAW_DATA, convertToFloatArray(this.accData));
accData.clear();
broadcastToSleepAsAndroid(intent);
}
/**
* Update the max raw data
* @param x the x value
* @param y the y value
* @param z the z value
*/
private void updateMaxRawData(float x, float y, float z) {
float maxRaw = calculateAccelerationMagnitude(x, y, z);
if (maxRaw > maxRawData) {
maxRawData = maxRaw;
}
}
/**
* Calculate the acceleration magnitude
* @param x the x value
* @param y the y value
* @param z the z value
* @return
*/
protected float calculateAccelerationMagnitude(float x, float y, float z) {
double sqrt = Math.sqrt((x * x) + (y * y) + (z * z));
return (float)sqrt;
}
/**
* On heart rate changed
*
* @param hr the heart rate
* @param sendDelay the send delay in ms. If 0 the data will be send right away. Anything bigger will gather all the data then send it all after the specified interval
*/
public void onHrChanged(float hr, long sendDelay) {
if (!isDeviceDefault() || !isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE) || !hasFeature(SleepAsAndroidFeature.HEART_RATE) || !trackingOngoing)
return;
if (trackingPaused) return;
updateLastHrData(hr);
if (lastHrDataMs == 0) {
lastHrDataMs = System.currentTimeMillis();
}
long ms = System.currentTimeMillis();
if (ms - lastHrDataMs >= sendDelay) {
lastHrDataMs = ms;
sendHrData();
}
}
/**
* Send the heart rate data
*/
private synchronized void sendHrData() {
LOG.debug("Sending heart rate data: " + this.hrData);
Intent intent = new Intent(ACTION_HEART_RATE_DATA_UPDATE);
intent.putExtra(DATA, convertToFloatArray(this.hrData));
broadcastToSleepAsAndroid(intent);
this.hrData.clear();
}
/**
* Update the last heart rate data
* @param hr the heart rate
*/
private void updateLastHrData(float hr) {
this.hrData.add(hr);
}
/**
* This is a generic intent to carry data from various sensors.
* See Sleep As Android documentation for parameters values.
* <a href="https://docs.sleep.urbandroid.org/devs/wearable_api.html#send-various-body-sensors-data-hr-rr-spo2-sdnn">...</a>
*/
public synchronized void sendExtra(Float hr, Long extraDataTimestamp, Long extraDataFramerate, ArrayList<Float> extraDataRR, ArrayList<Float> spo2Batch, Float sdnn, ArrayList<Float> extraDataBatch) {
if (!isDeviceDefault() || !trackingOngoing) return;
if (trackingPaused) return;
Context context = GBApplication.getContext();
Intent intent = new Intent(ACTION_EXTRA_DATA_UPDATE);
// Heart Rate
if (hr != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
intent.putExtra(EXTRA_DATA_HR, hr);
}
// SpO2
if (spo2Batch != null && (hasFeature(SleepAsAndroidFeature.SPO2) && isFeatureEnabled(SleepAsAndroidFeature.SPO2))) {
intent.putExtra(EXTRA_DATA_SPO2, true);
intent.putExtra(EXTRA_DATA_BATCH, convertToFloatArray(spo2Batch));
}
// SDNN
if (sdnn != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
intent.putExtra(EXTRA_DATA_SDNN, sdnn);
}
// RR Intervals
if (extraDataRR != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
intent.putExtra(EXTRA_DATA_RR, convertToFloatArray(extraDataRR));
}
if (extraDataBatch != null) {
if (isDeviceDefault()) {
for (int i = 0; i < extraDataBatch.size(); i++) {
extraDataBatch.set(i, 0.0f);
}
}
intent.putExtra(EXTRA_DATA_BATCH, convertToFloatArray(extraDataBatch));
}
if (extraDataTimestamp != null) {
intent.putExtra(EXTRA_DATA_TIMESTAMP, extraDataTimestamp);
}
if (extraDataFramerate != null) {
intent.putExtra(EXTRA_DATA_FRAMERATE, extraDataFramerate);
}
context.sendBroadcast(intent);
}
/**
* This is a generic intent to carry data from various sensors.
* See Sleep As Android documentation for parameters values.
* <a href="https://docs.sleep.urbandroid.org/devs/wearable_api.html#send-various-body-sensors-data-hr-rr-spo2-sdnn">...</a>
*/
public void sendExtra(Float hr, Float extraDataRR, Float spo2, Float sdnn, Long sdnnTimestamp) {
if (!isDeviceDefault() || !trackingOngoing) return;
if (trackingPaused) return;
Intent intent = new Intent(ACTION_EXTRA_DATA_UPDATE);
// Heart Rate
if (hr != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
intent.putExtra(EXTRA_DATA_HR, hr);
}
// SpO2
if (spo2 != null && (hasFeature(SleepAsAndroidFeature.SPO2) && isFeatureEnabled(SleepAsAndroidFeature.SPO2))) {
intent.putExtra(EXTRA_DATA_SPO2, spo2);
}
// SDNN
if (sdnn != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
intent.putExtra(EXTRA_DATA_SDNN, sdnn);
}
if (extraDataRR != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
intent.putExtra(EXTRA_DATA_RR, extraDataRR);
}
if (sdnnTimestamp != null) {
intent.putExtra(EXTRA_DATA_TIMESTAMP, sdnnTimestamp);
}
broadcastToSleepAsAndroid(intent);
}
/**
* Adds 5 minutes of tracking pause time
*/
public void sendPauseTracking() {
if (!isDeviceDefault() || !trackingOngoing) return;
LOG.debug("Sending pause");
Intent intent = new Intent(ACTION_PAUSE_FROM_WATCH);
broadcastToSleepAsAndroid(intent);
}
/**
* Resume tracking
*/
public void sendResumeTracking() {
if (!isDeviceDefault() || !trackingOngoing) return;
LOG.debug("Sending resume");
Intent intent = new Intent(ACTION_RESUME_FROM_WATCH);
broadcastToSleepAsAndroid(intent);
}
/**
* Snooze the current alarm
*/
public void sendSnoozeAlarm() {
if (!isDeviceDefault()) return;
if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) return;
LOG.debug("Sending snooze");
Intent intent = new Intent(ACTION_SNOOZE_FROM_WATCH);
broadcastToSleepAsAndroid(intent);
}
/**
* Dismiss the current alarm
*/
public void sendDismissAlarm() {
if (!isDeviceDefault()) return;
if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) return;
LOG.debug("Sending dismiss");
Intent intent = new Intent(ACTION_DISMISS_FROM_WATCH);
broadcastToSleepAsAndroid(intent);
}
private void broadcastToSleepAsAndroid(Intent intent) {
if (!isDeviceDefault()) return;
intent.setPackage(PACKAGE_SLEEP_AS_ANDROID);
GBApplication.getContext().sendBroadcast(intent);
}
/**
* Check if the device is set as default provider for Sleep As Android
*
* @return true if the device is set as default
*/
public boolean isDeviceDefault() {
if (device == null || !device.isInitialized()) return false;
if (isSleepAsAndroidEnabled()) {
return device.getAddress().equals(GBApplication.getPrefs().getString("sleepasandroid_device", ""));
}
return false;
}
public boolean isSleepAsAndroidEnabled() {
return GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_enable", false);
}
/**
* Validate if the device is allowed to receive a specific action
* @param action the action send my the broadcast receiver
*/
public void validateAction(String action) {
if (isDeviceDefault()) {
switch (action) {
case SleepAsAndroidAction.HINT:
case SleepAsAndroidAction.SHOW_NOTIFICATION:
if (!hasFeature(SleepAsAndroidFeature.NOTIFICATIONS) || !isFeatureEnabled(SleepAsAndroidFeature.NOTIFICATIONS)) {
throw new UnsupportedOperationException("Action not valid");
}
break;
case SleepAsAndroidAction.START_ALARM:
case SleepAsAndroidAction.STOP_ALARM:
case SleepAsAndroidAction.UPDATE_ALARM:
if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) {
throw new UnsupportedOperationException("Action not valid");
}
break;
case SleepAsAndroidAction.START_TRACKING:
case SleepAsAndroidAction.STOP_TRACKING:
case SleepAsAndroidAction.SET_BATCH_SIZE:
if (!hasFeature(SleepAsAndroidFeature.ACCELEROMETER) || !isFeatureEnabled(SleepAsAndroidFeature.ACCELEROMETER)) {
throw new UnsupportedOperationException("Action not valid");
}
break;
default:
break;
}
} else {
throw new UnsupportedOperationException("Action not valid");
}
}
/**
* Convert an ArrayList to a float array
*
* @param list the ArrayList
* @return the float array
*/
private float[] convertToFloatArray(ArrayList<Float> list) {
float[] result = new float[list.size()];
int i = 0;
for (float f : list) {
result[i++] = f;
}
return result;
}
/**
* Get configured alarm slot
*
* @return the alarm slot to be used for setting alarms on the watch
*/
public static int getAlarmSlot() {
Prefs prefs = GBApplication.getPrefs();
String slotString = prefs.getString("sleepasandroid_alarm_slot", "");
if (!slotString.isEmpty()) {
return Integer.parseInt(slotString);
}
return 0;
}
}

View File

@ -120,8 +120,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncState;
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncStateDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.LocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
@ -215,7 +215,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
if (!gpsUpdateSetup)
return;
LOG.info("Stop location updates");
GBLocationManager.stop(getContext(), this);
GBLocationService.stop(getContext(), getDevice());
gpsUpdateSetup = false;
}
@ -1140,14 +1140,14 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("Using combined GPS and NETWORK based location: " + onlyUseNetworkGPS);
if (!onlyUseNetworkGPS) {
try {
GBLocationManager.start(getContext(), this, LocationProviderType.GPS, intervalLength);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, intervalLength);
} catch (IllegalArgumentException e) {
LOG.warn("GPS provider could not be started", e);
}
}
try {
GBLocationManager.start(getContext(), this, LocationProviderType.NETWORK, intervalLength);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.NETWORK, intervalLength);
} catch (IllegalArgumentException e) {
LOG.warn("NETWORK provider could not be started", e);
}

View File

@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.location.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -26,6 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
@ -34,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSettingsService;
@ -60,6 +63,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetD
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetFileFlagsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SupportedFileTypesMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationSubscriptionStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@ -97,8 +101,10 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
@Override
public void dispose() {
super.dispose();
LOG.info("Garmin dispose()");
GBLocationService.stop(getContext(), getDevice());
stopMusicTimer();
super.dispose();
}
private void stopMusicTimer() {
@ -231,20 +237,39 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
} else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
notificationsHandler.setEnabled(enable);
LOG.info("NOTIFICATIONS ARE NOW {}", enable ? "ON" : "OFF");
final NotificationSubscriptionStatusMessage.NotificationStatus finalStatus;
if (getDevicePrefs().getBoolean(PREF_SEND_APP_NOTIFICATIONS, true)) {
finalStatus = NotificationSubscriptionStatusMessage.NotificationStatus.ENABLED;
} else {
finalStatus = NotificationSubscriptionStatusMessage.NotificationStatus.DISABLED;
}
LOG.info("NOTIFICATIONS ARE NOW enabled={}, status={}", enable, finalStatus);
sendOutgoingMessage(new NotificationSubscriptionStatusMessage(
GFDIMessage.Status.ACK,
finalStatus,
enable,
0
));
} else if (deviceEvent instanceof SupportedFileTypesDeviceEvent) {
this.supportedFileTypeList.clear();
this.supportedFileTypeList.addAll(((SupportedFileTypesDeviceEvent) deviceEvent).getSupportedFileTypes());
} else if (deviceEvent instanceof FileDownloadedDeviceEvent) {
LOG.debug("FILE DOWNLOAD COMPLETE {}", ((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileName());
if (false) // delete file from watch upon successful download TODO: add device setting
if (!getKeepActivityDataOnDevice()) // delete file from watch upon successful download
sendOutgoingMessage(new SetFileFlagsMessage(((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
}
super.evaluateGBDeviceEvent(deviceEvent);
}
private boolean getKeepActivityDataOnDevice() {
return getDevicePrefs().getBoolean("keep_activity_data_on_device", true); // TODO: change to default false once we are sure of the consequences
}
@Override
public void onFetchRecordedData(final int dataTypes) {
if (this.supportedFileTypeList.isEmpty()) {
@ -258,14 +283,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (!getDevicePrefs().getBoolean(PREF_SEND_APP_NOTIFICATIONS, true)) {
// FIXME: Instead of silently dropping the notification, use NotificationSubscriptionMessage
// to signal to the watch that they're disabled
LOG.debug("App notifications disabled - ignoring");
return;
}
public void onNotification(final NotificationSpec notificationSpec) {
sendOutgoingMessage(notificationsHandler.onNotification(notificationSpec));
}
@ -410,8 +428,15 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
@Override
public void onSendConfiguration(String config) {
if (PREF_GARMIN_DEFAULT_REPLY_SUFFIX.equals(config)) {
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
switch (config) {
case PREF_GARMIN_DEFAULT_REPLY_SUFFIX:
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
break;
case PREF_SEND_APP_NOTIFICATIONS:
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
notificationSubscriptionDeviceEvent.enable = true; // actual status is fetched from preferences
evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent);
break;
}
}
@ -427,7 +452,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove();
while (checkFileExists(directoryEntry.getFileName())) {
LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName());
if (false) // delete file from watch if already downloaded TODO: add device setting
if (!getKeepActivityDataOnDevice()) // delete file from watch if already downloaded
sendOutgoingMessage(new SetFileFlagsMessage(directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
directoryEntry = filesToDownload.remove();
}
@ -578,5 +603,18 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
return dir;
}
@Override
public void onSetGpsLocation(final Location location) {
final GdiCore.CoreService.LocationUpdatedNotification.Builder locationUpdatedNotification = GdiCore.CoreService.LocationUpdatedNotification.newBuilder()
.addLocationData(
GarminUtils.toLocationData(location, GdiCore.CoreService.DataType.REALTIME_TRACKING)
);
final ProtobufMessage locationUpdatedNotificationRequest = protocolBufferHandler.prepareProtobufRequest(
GdiSmartProto.Smart.newBuilder().setCoreService(
GdiCore.CoreService.newBuilder().setLocationUpdatedNotification(locationUpdatedNotification)
).build()
);
sendOutgoingMessage(locationUpdatedNotificationRequest);
}
}

View File

@ -0,0 +1,35 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.location.Location;
import android.os.Build;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
public final class GarminUtils {
private GarminUtils() {
// utility class
}
public static GdiCore.CoreService.LocationData toLocationData(final Location location, final GdiCore.CoreService.DataType dataType) {
final GdiCore.CoreService.LatLon positionForWatch = GdiCore.CoreService.LatLon.newBuilder()
.setLat((int) ((location.getLatitude() * 2.147483648E9d) / 180.0d))
.setLon((int) ((location.getLongitude() * 2.147483648E9d) / 180.0d))
.build();
float vAccuracy = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vAccuracy = location.getVerticalAccuracyMeters();
}
return GdiCore.CoreService.LocationData.newBuilder()
.setPosition(positionForWatch)
.setAltitude((float) location.getAltitude())
.setTimestamp(GarminTimeUtils.javaMillisToGarminTimestamp(location.getTime()))
.setHAccuracy(location.getAccuracy())
.setVAccuracy(vAccuracy)
.setPositionType(dataType)
.setBearing(location.getBearing())
.setSpeed(location.getSpeed())
.build();
}
}

View File

@ -55,7 +55,7 @@ public class NotificationsHandler implements MessageHandler {
final byte[] bytes = entry.getKey().getNotificationSpecAttribute(notificationSpec, entry.getValue());
messageWriter.writeShort(bytes.length);
messageWriter.writeBytes(bytes);
// LOG.info("ATTRIBUTE:{} value:{} length:{}", entry.getKey(), new String(bytes), bytes.length);
// LOG.info("ATTRIBUTE:{} value:{}/{} length:{}", entry.getKey(), new String(bytes), GB.hexdump(bytes), bytes.length);
}
@ -183,7 +183,7 @@ public class NotificationsHandler implements MessageHandler {
case REPLY_MESSAGES:
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
deviceEvtNotificationControl.reply = message.getActionString();
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) {
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE) || notificationSpec.type.equals(NotificationType.GENERIC_SMS)) {
deviceEvtNotificationControl.phoneNumber = notificationSpec.phoneNumber;
} else {
deviceEvtNotificationControl.handle = mNotificationReplyAction.lookup(notificationSpec.getId()); //handle of wearable action is needed
@ -267,7 +267,7 @@ public class NotificationsHandler implements MessageHandler {
MESSAGE_SIZE(4),
DATE(5),
// POSITIVE_ACTION_LABEL(6), //needed only for legacy notification actions
// NEGATIVE_ACTION_LABEL(7), //needed only for legacy notification actions
NEGATIVE_ACTION_LABEL(7), //needed only for legacy notification actions
// Garmin extensions
// PHONE_NUMBER(126, true),
ACTIONS(127, false, true),
@ -312,7 +312,10 @@ public class NotificationsHandler implements MessageHandler {
toReturn = NOTIFICATION_DATE_FORMAT.format(new Date(notificationTimestamp));
break;
case TITLE:
toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
if (NotificationType.GENERIC_SMS.equals(notificationSpec.type))
toReturn = notificationSpec.sender == null ? "" : notificationSpec.sender;
else
toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
break;
case SUBTITLE:
toReturn = notificationSpec.subject == null ? "" : notificationSpec.subject;
@ -347,6 +350,7 @@ public class NotificationsHandler implements MessageHandler {
for (NotificationSpec.Action action : notificationSpec.attachedActions) {
switch (action.type) {
case NotificationSpec.Action.TYPE_WEARABLE_REPLY:
case NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR:
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_MESSAGES, action.title));
break;
case NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS:

View File

@ -1,9 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.location.Location;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -12,8 +13,14 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCalendarService;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
@ -26,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.HttpHand
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
@ -38,7 +46,7 @@ public class ProtocolBufferHandler implements MessageHandler {
private final int maxChunkSize = 375; //tested on VĂ­vomove Style
private int lastProtobufRequestId;
private Map<GdiSmsNotification.SmsNotificationService.CannedListType, String[]> cannedListTypeMap;
private final Map<GdiSmsNotification.SmsNotificationService.CannedListType, String[]> cannedListTypeMap = new HashMap<>();
public ProtocolBufferHandler(GarminSupport deviceSupport) {
this.deviceSupport = deviceSupport;
@ -74,9 +82,7 @@ public class ProtocolBufferHandler implements MessageHandler {
}
boolean processed = false;
if (smart.hasCoreService()) { //TODO: unify request and response???
processed = true;
processProtobufCoreResponse(smart.getCoreService());
// return prepareProtobufResponse(processProtobufCoreRequest(smart.getCoreService()), message.getRequestId());
return prepareProtobufResponse(processProtobufCoreRequest(smart.getCoreService()), message.getRequestId());
}
if (smart.hasCalendarService()) {
return prepareProtobufResponse(processProtobufCalendarRequest(smart.getCalendarService()), message.getRequestId());
@ -154,16 +160,33 @@ public class ProtocolBufferHandler implements MessageHandler {
LOG.debug("CalendarService Skipping event {} that is out of requested time range", mEvt.getTitle());
continue;
}
if (!calendarServiceRequest.getIncludeAllDay() && mEvt.isAllDay()) {
LOG.debug("CalendarService Skipping event {} that is AllDay", mEvt.getTitle());
continue;
}
watchEvents.add(GdiCalendarService.CalendarService.CalendarEvent.newBuilder()
.setTitle(mEvt.getTitle())
if (watchEvents.size() >= calendarServiceRequest.getMaxEvents() * 2) { //NOTE: Tested with values higher than double of the reported max without issues
LOG.debug("Reached the maximum number of events supported by the watch");
break;
}
final GdiCalendarService.CalendarService.CalendarEvent.Builder event = GdiCalendarService.CalendarService.CalendarEvent.newBuilder()
.setTitle(mEvt.getTitle().substring(0, Math.min(mEvt.getTitle().length(), calendarServiceRequest.getMaxTitleLength())))
.setAllDay(mEvt.isAllDay())
.setBegin(mEvt.getBeginSeconds())
.setEnd(mEvt.getEndSeconds())
.setLocation(StringUtils.defaultString(mEvt.getLocation()))
.setDescription(StringUtils.defaultString(mEvt.getDescription()))
.build()
);
.setStartDate(mEvt.getBeginSeconds())
.setEndDate(mEvt.getEndSeconds());
if (calendarServiceRequest.getIncludeLocation() && mEvt.getLocation() != null) {
event.setLocation(mEvt.getLocation().substring(0, Math.min(mEvt.getLocation().length(), calendarServiceRequest.getMaxLocationLength())));
}
if (calendarServiceRequest.getIncludeDescription() && mEvt.getDescription() != null) {
event.setDescription(mEvt.getDescription().substring(0, Math.min(mEvt.getDescription().length(), calendarServiceRequest.getMaxDescriptionLength())));
}
if (calendarServiceRequest.getIncludeOrganizer() && mEvt.getOrganizer() != null) {
event.setDescription(mEvt.getOrganizer().substring(0, Math.min(mEvt.getOrganizer().length(), calendarServiceRequest.getMaxOrganizerLength())));
}
watchEvents.add(event.build());
}
LOG.debug("CalendarService Sending {} events to watch", watchEvents.size());
@ -171,7 +194,7 @@ public class ProtocolBufferHandler implements MessageHandler {
GdiCalendarService.CalendarService.newBuilder().setCalendarResponse(
GdiCalendarService.CalendarService.CalendarServiceResponse.newBuilder()
.addAllCalendarEvent(watchEvents)
.setUnknown(1)
.setStatus(GdiCalendarService.CalendarService.CalendarServiceResponse.ResponseStatus.OK)
)
).build();
}
@ -179,19 +202,11 @@ public class ProtocolBufferHandler implements MessageHandler {
return GdiSmartProto.Smart.newBuilder().setCalendarService(
GdiCalendarService.CalendarService.newBuilder().setCalendarResponse(
GdiCalendarService.CalendarService.CalendarServiceResponse.newBuilder()
.setUnknown(0)
.setStatus(GdiCalendarService.CalendarService.CalendarServiceResponse.ResponseStatus.UNKNOWN_RESPONSE_STATUS)
)
).build();
}
private void processProtobufCoreResponse(GdiCore.CoreService coreService) {
if (coreService.hasSyncResponse()) {
final GdiCore.CoreService.SyncResponse syncResponse = coreService.getSyncResponse();
LOG.info("Received sync status: {}", syncResponse.getStatus());
}
LOG.warn("Unknown CoreService response: {}", coreService);
}
private void processProtobufDeviceStatusResponse(GdiDeviceStatus.DeviceStatusService deviceStatusService) {
if (deviceStatusService.hasRemoteDeviceBatteryStatusResponse()) {
final GdiDeviceStatus.DeviceStatusService.RemoteDeviceBatteryStatusResponse batteryStatusResponse = deviceStatusService.getRemoteDeviceBatteryStatusResponse();
@ -210,38 +225,90 @@ public class ProtocolBufferHandler implements MessageHandler {
LOG.warn("Unknown DeviceStatusService response: {}", deviceStatusService);
}
// private GdiSmartProto.Smart processProtobufCoreRequest(GdiCore.CoreService coreService) {
// if (coreService.hasLocationUpdatedSetEnabledRequest()) { //TODO: enable location support in devicesupport
// LOG.debug("Location CoreService: {}", coreService);
//
// final GdiCore.CoreService.LocationUpdatedSetEnabledRequest locationUpdatedSetEnabledRequest = coreService.getLocationUpdatedSetEnabledRequest();
//
// LOG.info("Received locationUpdatedSetEnabledRequest status: {}", locationUpdatedSetEnabledRequest.getEnabled());
//
// GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Builder response = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.newBuilder()
// .setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Status.OK);
//
// //TODO: check and follow the preference in coordinator (see R.xml.devicesettings_workout_send_gps_to_band )
// if(locationUpdatedSetEnabledRequest.getEnabled()) {
// response.addRequests(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.newBuilder()
// .setRequested(locationUpdatedSetEnabledRequest.getRequests(0).getRequested())
// .setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.OK));
// }
//
// deviceSupport.processLocationUpdateRequest(locationUpdatedSetEnabledRequest.getEnabled(), locationUpdatedSetEnabledRequest.getRequestsList());
//
// return GdiSmartProto.Smart.newBuilder().setCoreService(
// GdiCore.CoreService.newBuilder().setLocationUpdatedSetEnabledResponse(response)).build();
// }
// LOG.warn("Unknown CoreService request: {}", coreService);
// return null;
// }
private GdiSmartProto.Smart processProtobufCoreRequest(GdiCore.CoreService coreService) {
if (coreService.hasSyncResponse()) {
final GdiCore.CoreService.SyncResponse syncResponse = coreService.getSyncResponse();
LOG.info("Received sync status: {}", syncResponse.getStatus());
return null;
}
if (coreService.hasGetLocationRequest()) {
LOG.info("Got location request");
final Location location = new CurrentPosition().getLastKnownLocation();
final GdiCore.CoreService.GetLocationResponse.Builder response = GdiCore.CoreService.GetLocationResponse.newBuilder();
if (location.getLatitude() == 0 && location.getLongitude() == 0) {
response.setStatus(GdiCore.CoreService.GetLocationResponse.Status.NO_VALID_LOCATION);
} else {
response.setStatus(GdiCore.CoreService.GetLocationResponse.Status.OK)
.setLocationData(GarminUtils.toLocationData(location, GdiCore.CoreService.DataType.GENERAL_LOCATION));
}
return GdiSmartProto.Smart.newBuilder().setCoreService(
GdiCore.CoreService.newBuilder().setGetLocationResponse(response)).build();
}
if (coreService.hasLocationUpdatedSetEnabledRequest()) {
final GdiCore.CoreService.LocationUpdatedSetEnabledRequest locationUpdatedSetEnabledRequest = coreService.getLocationUpdatedSetEnabledRequest();
LOG.info("Received locationUpdatedSetEnabledRequest status: {}", locationUpdatedSetEnabledRequest.getEnabled());
GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Builder response = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.newBuilder()
.setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Status.OK);
final boolean sendGpsPref = deviceSupport.getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false);
GdiCore.CoreService.Request realtimeRequest = null;
if (locationUpdatedSetEnabledRequest.getEnabled()) {
for (final GdiCore.CoreService.Request request : locationUpdatedSetEnabledRequest.getRequestsList()) {
final GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus requestedStatus;
if (GdiCore.CoreService.DataType.REALTIME_TRACKING.equals(request.getRequested())) {
realtimeRequest = request;
if (sendGpsPref) {
requestedStatus = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.OK;
} else {
requestedStatus = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.KO;
}
} else {
requestedStatus = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.KO;
}
response.addRequests(
GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.newBuilder()
.setRequested(request.getRequested())
.setStatus(requestedStatus)
);
}
}
if (sendGpsPref) {
if (realtimeRequest != null) {
GBLocationService.start(
deviceSupport.getContext(),
deviceSupport.getDevice(),
GBLocationProviderType.GPS,
1000 // TODO from realtimeRequest
);
} else {
GBLocationService.stop(deviceSupport.getContext(), deviceSupport.getDevice());
}
}
return GdiSmartProto.Smart.newBuilder().setCoreService(
GdiCore.CoreService.newBuilder().setLocationUpdatedSetEnabledResponse(response)).build();
}
LOG.warn("Unknown CoreService request: {}", coreService);
return null;
}
private GdiSmartProto.Smart processProtobufSmsNotificationMessage(GdiSmsNotification.SmsNotificationService smsNotificationService) {
if (smsNotificationService.hasSmsCannedListRequest()) {
if (null == this.cannedListTypeMap || this.cannedListTypeMap.isEmpty()) {
this.cannedListTypeMap = new HashMap<>();
LOG.debug("Got request for sms canned list");
// Mark canned messages as supported
deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, true));
if (this.cannedListTypeMap.isEmpty()) {
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
for (GdiSmsNotification.SmsNotificationService.CannedListType type :
requestedTypes) {
@ -277,7 +344,7 @@ public class ProtocolBufferHandler implements MessageHandler {
for (GdiSmsNotification.SmsNotificationService.CannedListType requestedType : requestedTypes) {
if (this.cannedListTypeMap.containsKey(requestedType)) {
builder.addLists(GdiSmsNotification.SmsNotificationService.SmsCannedList.newBuilder()
.addAllResponse(Arrays.asList(this.cannedListTypeMap.get(requestedType)))
.addAllResponse(Arrays.asList(Objects.requireNonNull(this.cannedListTypeMap.get(requestedType))))
.setType(requestedType)
);
} else {
@ -348,9 +415,6 @@ public class ProtocolBufferHandler implements MessageHandler {
return null;
}
if (null == this.cannedListTypeMap) {
this.cannedListTypeMap = new HashMap<>();
}
this.cannedListTypeMap.put(cannedListType, cannedMessagesSpec.cannedMessages);
GdiSmartProto.Smart smart = GdiSmartProto.Smart.newBuilder()

View File

@ -1,7 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -77,7 +77,7 @@ public class NotificationControlMessage extends GFDIMessage {
}
private static Map<NotificationsHandler.NotificationAttribute, Integer> createGetNotificationAttributesCommand(MessageReader reader) {
final Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = new HashMap<>();
final Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = new LinkedHashMap<>();
while (reader.remaining() > 0) {
final int attributeID = reader.readByte();

View File

@ -17,7 +17,8 @@ public class NotificationSubscriptionMessage extends GFDIMessage {
this.enable = enable;
this.unk = unk;
this.statusMessage = new NotificationSubscriptionStatusMessage(Status.ACK, NotificationSubscriptionStatusMessage.NotificationStatus.OK, enable, unk);
// We do not set the status message here so we can reply with the proper notifications status
// from the device event
}
public static NotificationSubscriptionMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {

View File

@ -34,17 +34,64 @@ public class NotificationUpdateMessage extends GFDIMessage {
writer.writeByte(getCategoryValue(this.notificationType));
writer.writeByte(this.count);
writer.writeInt(this.notificationId);
writer.writeByte(this.useLegacyActions ? 0x00 : 0x03);
writer.writeByte(getNotificationPhoneFlags());
return true;
}
private int getNotificationPhoneFlags() {
EnumSet<NotificationPhoneFlags> flags = EnumSet.noneOf(NotificationPhoneFlags.class);
if (this.hasActions)
flags.add(NotificationPhoneFlags.NEW_ACTIONS);
if (this.useLegacyActions)
flags.add(NotificationPhoneFlags.LEGACY_ACTIONS);
return (int) EnumUtils.generateBitVector(NotificationPhoneFlags.class, flags);
}
//no image
//00 updatetype
// 12 flags
// 00 notif type
// 00 count
// 03000000
// 02
//image
//00
// 12
// 00
// 00
// 04000000
// 06
//0F00
// A913
// 00
// 12
// 0C
// 00
// 471D2A66
// 02
// BC14
//0F00
// A913
// 00
// 11
// 00
// 00
// 461D2A66
// 00
// 8C00
private int getCategoryFlags(NotificationType notificationType) {
EnumSet<NotificationFlag> flags = EnumSet.noneOf(NotificationFlag.class);
if (this.hasActions && this.useLegacyActions) { //only needed for legacy actions
flags.add(NotificationFlag.ACTION_ACCEPT);
flags.add(NotificationFlag.ACTION_DECLINE);
}
flags.add(NotificationFlag.ACTION_DECLINE);
switch (notificationType.getGenericType()) {
case "generic_phone":
@ -111,5 +158,13 @@ public class NotificationUpdateMessage extends GFDIMessage {
LOCATION,
ENTERTAINMENT,
SMS
}
enum NotificationPhoneFlags {
LEGACY_ACTIONS,
NEW_ACTIONS,
HAS_ATTACHMENTS,
;
}
}

View File

@ -33,7 +33,8 @@ public class NotificationSubscriptionStatusMessage extends GFDIStatusMessage {
}
public enum NotificationStatus {
OK,
ENABLED,
DISABLED
;
public static NotificationStatus fromId(int id) {

View File

@ -63,7 +63,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
@ -117,7 +116,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.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
@ -128,6 +128,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.AbstractFetchOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchStatisticsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchTemperatureOperation;
@ -346,6 +347,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
private final LinkedList<AbstractFetchOperation> fetchOperationQueue = new LinkedList<>();
protected SleepAsAndroidSender sleepAsAndroidSender;
public HuamiSupport() {
this(LOG);
}
@ -372,6 +374,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
public void setContext(final GBDevice gbDevice, final BluetoothAdapter btAdapter, final Context context) {
super.setContext(gbDevice, btAdapter, context);
this.mediaManager = new MediaManager(context);
}
@Override
@ -406,6 +409,9 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
} else {
new InitOperation(authenticate, authFlags, cryptFlags, this, builder).perform();
}
if (sleepAsAndroidSender == null) {
sleepAsAndroidSender = new SleepAsAndroidSender(gbDevice);
}
characteristicHRControlPoint = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT);
characteristicChunked = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER);
} catch (IOException e) {
@ -2004,7 +2010,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
if (sendGpsToBand) {
lastPhoneGpsSent = 0;
sendPhoneGps(HuamiPhoneGpsStatus.SEARCHING, null);
GBLocationManager.start(getContext(), this);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
} else {
sendPhoneGps(HuamiPhoneGpsStatus.DISABLED, null);
}
@ -2024,7 +2030,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
protected void onWorkoutEnd() {
final boolean startOnPhone = HuamiCoordinator.getWorkoutStartOnPhone(getDevice().getAddress());
GBLocationManager.stop(getContext(), this);
GBLocationService.stop(getContext(), getDevice());
if (startOnPhone) {
LOG.info("Stopping OpenTracks recording");
@ -2612,6 +2618,8 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
MiBand2SampleProvider provider = new MiBand2SampleProvider(gbDevice, session);
MiBandActivitySample sample = createActivitySample(device, user, ts, provider);
sample.setHeartRate(getHeartrateBpm());
sleepAsAndroidSender.onHrChanged(sample.getHeartRate(), 0);
// sample.setSteps(getSteps());
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
sample.setRawKind(HuamiConst.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that?

View File

@ -17,6 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos;
import static org.apache.commons.lang3.ArrayUtils.subarray;
import static java.lang.Thread.sleep;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service.*;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.SUCCESS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_NAME;
@ -41,6 +42,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.widget.Toast;
@ -56,6 +58,7 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
@ -67,6 +70,8 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -79,6 +84,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSilentMode;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service;
@ -91,6 +97,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidAction;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
@ -139,6 +146,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWatchfaceService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWifiService;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
@ -151,9 +159,10 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
// Tracks whether realtime HR monitoring is already started, so we can just
// send CONTINUE commands
private boolean heartRateRealtimeStarted;
private ScheduledExecutorService heartRateRealtimeScheduler;
// Keep track of whether the rawSensor is enabled
private boolean rawSensor = false;
private ScheduledExecutorService rawSensorScheduler;
// Services
private final ZeppOsServicesService servicesService = new ZeppOsServicesService(this);
@ -867,6 +876,169 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
}
}
@Override
public void onSleepAsAndroidAction(String action, Bundle extras) {
// Validate if our device can work with an action
try {
sleepAsAndroidSender.validateAction(action);
} catch (UnsupportedOperationException e) {
return;
}
// Consult the SleepAsAndroid documentation for a set of actions and their extra
// https://docs.sleep.urbandroid.org/devs/wearable_api.html
switch (action) {
case SleepAsAndroidAction.CHECK_CONNECTED:
sleepAsAndroidSender.confirmConnected();
break;
// Received when the app starts sleep tracking
case SleepAsAndroidAction.START_TRACKING:
enableRealtimeHeartRateMeasurement(true);
enableRawSensor(true);
sleepAsAndroidSender.startTracking();
break;
// Received when the app stops sleep tracking
case SleepAsAndroidAction.STOP_TRACKING:
enableRealtimeHeartRateMeasurement(false);
enableRawSensor(false);
sleepAsAndroidSender.stopTracking();
break;
// Received when the app pauses sleep tracking
// case SleepAsAndroidAction.SET_PAUSE:
// long pauseTimestamp = extras.getLong("TIMESTAMP");
// long delay = pauseTimestamp > 0 ? pauseTimestamp - System.currentTimeMillis() : 0;
// setRawSensor(delay > 0);
// enableRealtimeSamplesTimer(delay > 0);
// sleepAsAndroidSender.pauseTracking(delay);
// break;
// Same as above but controlled by a boolean value
case SleepAsAndroidAction.SET_SUSPENDED:
boolean suspended = extras.getBoolean("SUSPENDED", false);
setRawSensor(!suspended);
enableRealtimeSamplesTimer(!suspended);
sleepAsAndroidSender.pauseTracking(suspended);
// Received when the app changes the batch size for the movement data
case SleepAsAndroidAction.SET_BATCH_SIZE:
long batchSize = extras.getLong("SIZE", 12L);
sleepAsAndroidSender.setBatchSize(batchSize);
break;
// Received when the app requests the wearable to vibrate
case SleepAsAndroidAction.HINT:
int repeat = extras.getInt("REPEAT");
for (int i = 0; i < repeat; i++) {
sendFindDeviceCommand(true);
try {
sleep(500);
sendFindDeviceCommand(false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
break;
// Received when the app sends a notificaation
case SleepAsAndroidAction.SHOW_NOTIFICATION:
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.title = extras.getString("TITLE");
notificationSpec.body = extras.getString("BODY");
notificationService.sendNotification(notificationSpec);
break;
// Received when the app updates an alarm (Snoozing included too)
// It's better to use SleepAsAndroidAction.START_ALARM and .STOP_ALARM where possible to have more control over the alarm.
// Using .UPDATE_ALARM will let Gadgetbridge know when an alarm was set but not when it was dismissed.
case SleepAsAndroidAction.UPDATE_ALARM:
long alarmTimestamp = extras.getLong("TIMESTAMP");
// Sets the alarm at a giver hour and minute
// Snoozing from the app will create a new alarm in the future
setSleepAsAndroidAlarm(alarmTimestamp);
break;
// Received when an app alarm is stopped
case SleepAsAndroidAction.STOP_ALARM:
// Manually stop an alarm
break;
// Received when an app alarm starts
case SleepAsAndroidAction.START_ALARM:
// Manually start an alarm
break;
default:
LOG.warn("Received unsupported " + action);
break;
}
}
private void setSleepAsAndroidAlarm(long alarmTimestamp) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(new Timestamp(alarmTimestamp).getTime());
Alarm alarm = AlarmUtils.createSingleShot(SleepAsAndroidSender.getAlarmSlot(), false, false, calendar);
ArrayList<Alarm> alarms = new ArrayList<>(1);
alarms.add(alarm);
GBApplication.deviceService(gbDevice).onSetAlarms(alarms);
}
private ScheduledExecutorService startRealtimeHeartRateMeasurement() {
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (heartRateRealtimeStarted) {
onEnableRealtimeHeartRateMeasurement(true);
}
}
}, 0, 1000, TimeUnit.MILLISECONDS);
return service;
}
private void stopRealtimeHeartRateMeasurement() {
if (heartRateRealtimeScheduler != null) {
heartRateRealtimeScheduler.shutdown();
heartRateRealtimeScheduler = null;
}
}
private void enableRealtimeHeartRateMeasurement(boolean enable) {
onEnableRealtimeHeartRateMeasurement(enable);
if (enable) {
heartRateRealtimeScheduler = startRealtimeHeartRateMeasurement();
}
else {
stopRealtimeHeartRateMeasurement();
}
}
private void stopRawSensors() {
if (rawSensorScheduler != null) {
rawSensorScheduler.shutdown();
rawSensorScheduler = null;
}
}
private ScheduledExecutorService startRawSensors() {
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (rawSensor) {
setRawSensor(true);
}
}
}, 0, 10000, TimeUnit.MILLISECONDS);
return service;
}
private void enableRawSensor(boolean enable) {
setRawSensor(enable);
if (enable) {
rawSensorScheduler = startRawSensors();
}
else {
stopRawSensors();
}
}
@Override
protected ZeppOsSupport setTimeFormat(final TransactionBuilder builder) {
final GBPrefs gbPrefs = new GBPrefs(getDevicePrefs());
@ -1136,6 +1308,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
final float gx = (x * gravity) / scaleFactor;
final float gy = (y * gravity) / scaleFactor;
final float gz = (z * gravity) / scaleFactor;
sleepAsAndroidSender.onAccelChanged(gx, gy, gz);
LOG.info("Raw sensor g: x={} y={} z={}", gx, gy, gz);
}

View File

@ -143,10 +143,12 @@ public class ZeppOsCalendarService extends AbstractZeppOsService {
buf.putInt(calendarEventSpec.timestamp + calendarEventSpec.durationInSeconds);
// Remind
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
if (calendarEventSpec.reminders != null && !calendarEventSpec.reminders.isEmpty()) {
buf.putInt((int) (calendarEventSpec.reminders.get(0) / 1000L));
} else {
buf.putInt(0);
}
// Repeat
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
@ -231,7 +233,10 @@ public class ZeppOsCalendarService extends AbstractZeppOsService {
final int endTime = BLETypeConversions.toUint32(payload, i);
i += 4;
// ? 00 00 00 00 00 00 00 00 ff ff ff ff
final int reminderTime = BLETypeConversions.toUint32(payload, i);
i += 4;
// ? 00 00 00 00 ff ff ff ff
i += 12;
boolean allDay = (payload[i] == 0x01);

View File

@ -49,8 +49,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationListener;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;

View File

@ -44,7 +44,6 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier;
@ -65,7 +64,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@ -228,11 +228,6 @@ public class HuaweiSupportProvider {
}
public void setGps(boolean start) {
EventHandler handler;
if (isBLE())
handler = leSupport;
else
handler = brSupport;
if (start) {
if (!GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false))
return;
@ -241,7 +236,7 @@ public class HuaweiSupportProvider {
gpsParameterRequest.setFinalizeReq(new RequestCallback() {
@Override
public void call() {
GBLocationManager.start(getContext(), handler);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
}
});
try {
@ -251,9 +246,9 @@ public class HuaweiSupportProvider {
LOG.error("Failed to get GPS parameters", e);
}
} else
GBLocationManager.start(getContext(), handler);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
} else
GBLocationManager.stop(getContext(), handler);
GBLocationService.stop(getContext(), getDevice());
}
public void setGpsParametersResponse(GpsAndTime.GpsParameters.Response response) {

View File

@ -21,7 +21,9 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
@ -37,8 +39,11 @@ public class SendAccountRequest extends Request {
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
String account = GBApplication
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_ACCOUNT, "").trim();
try {
return new AccountRelated.SendAccountToDevice.Request(paramsProvider).serialize();
return new AccountRelated.SendAccountToDevice.Request(paramsProvider, account).serialize();
} catch (CryptoException e) {
throw new RequestCreationException(e);
}

View File

@ -22,6 +22,8 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
@ -37,10 +39,14 @@ public class SendExtendedAccountRequest extends Request {
@Override
protected List<byte[]> createRequest() throws Request.RequestCreationException {
String account = GBApplication
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_ACCOUNT, "").trim();
try {
return new AccountRelated.SendExtendedAccountToDevice.Request(
paramsProvider,
supportProvider.getHuaweiCoordinator().supportsDiffAccountPairingOptimization())
supportProvider.getHuaweiCoordinator().supportsDiffAccountPairingOptimization(),
account)
.serialize();
} catch (CryptoException e) {
throw new Request.RequestCreationException(e);

View File

@ -106,6 +106,11 @@ public class XiaomiCalendarService extends AbstractXiaomiService {
thisSync.add(calendarEvent);
int notifyMinutesBefore = 0;
if (!calendarEvent.getRemindersAbsoluteTs().isEmpty()) {
notifyMinutesBefore = (int) ((calendarEvent.getBeginSeconds() * 1000L - calendarEvent.getRemindersAbsoluteTs().get(0)) / (1000 * 60));
}
final XiaomiProto.CalendarEvent xiaomiCalendarEvent = XiaomiProto.CalendarEvent.newBuilder()
.setTitle(calendarEvent.getTitle())
.setDescription(StringUtils.ensureNotNull(calendarEvent.getDescription()))
@ -113,7 +118,7 @@ public class XiaomiCalendarService extends AbstractXiaomiService {
.setStart(calendarEvent.getBeginSeconds())
.setEnd((int) (calendarEvent.getEnd() / 1000))
.setAllDay(calendarEvent.isAllDay())
.setNotifyMinutesBefore(0) // TODO fetch from event
.setNotifyMinutesBefore(notifyMinutesBefore)
.build();
calendarSync.addEvent(xiaomiCalendarEvent);

View File

@ -48,7 +48,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@ -664,7 +665,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
if (!gpsStarted) {
gpsStarted = true;
gpsFixAcquired = false;
GBLocationManager.start(getSupport().getContext(), getSupport());
GBLocationService.start(getSupport().getContext(), getSupport().getDevice(), GBLocationProviderType.GPS, 1000);
}
gpsTimeoutHandler.removeCallbacksAndMessages(null);
@ -673,7 +674,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
LOG.debug("Timed out waiting for workout");
gpsStarted = false;
gpsFixAcquired = false;
GBLocationManager.stop(getSupport().getContext(), getSupport());
GBLocationService.stop(getSupport().getContext(), getSupport().getDevice());
}, 5000);
}
@ -696,7 +697,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
case WORKOUT_FINISHED:
gpsStarted = false;
gpsFixAcquired = false;
GBLocationManager.stop(getSupport().getContext(), getSupport());
GBLocationService.stop(getSupport().getContext(), getSupport().getDevice());
if (startOnPhone) {
OpenTracksController.stopRecording(getSupport().getContext());
}

View File

@ -18,6 +18,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.serial;
import android.location.Location;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.UUID;

View File

@ -500,27 +500,6 @@ 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 = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false);
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_GPS)
.setTicker(context.getString(R.string.notification_gps_title))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.notification_gps_title))
.setContentText(context.getString(R.string.notification_gps_text, numDevices))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_gps_location)
.setOngoing(true);
notify(NOTIFICATION_ID_GPS, nb.build(), context);
}
public static void removeGpsNotification(Context context) {
removeNotification(NOTIFICATION_ID_GPS, context);
}
private static Notification createInstallNotification(String text, boolean ongoing,
int percentage, Context context) {
Intent notificationIntent = new Intent(context, ControlCenterv2.class);

View File

@ -16,21 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util.calendar;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class CalendarEvent {
private long begin;
private long end;
private long id;
private String title;
private String description;
private String location;
private String calName;
private String calAccountName;
private int color;
private boolean allDay;
private final long begin;
private final long end;
private final long id;
private final String title;
private final String description;
private final String location;
private final String calName;
private final String calAccountName;
private final String organizer;
private final int color;
private final boolean allDay;
private List<Long> remindersAbsoluteTs = new ArrayList<>();
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, String calAccountName, int color, boolean allDay) {
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, String calAccountName, int color, boolean allDay, String organizer) {
this.begin = begin;
this.end = end;
this.id = id;
@ -41,6 +45,15 @@ public class CalendarEvent {
this.calAccountName = calAccountName;
this.color = color;
this.allDay = allDay;
this.organizer = organizer;
}
public List<Long> getRemindersAbsoluteTs() {
return remindersAbsoluteTs;
}
public void setRemindersAbsoluteTs(List<Long> remindersAbsoluteTs) {
this.remindersAbsoluteTs = remindersAbsoluteTs;
}
public long getBegin() {
@ -80,6 +93,10 @@ public class CalendarEvent {
return title;
}
public String getOrganizer() {
return organizer;
}
public String getDescription() {
return description;
}
@ -121,7 +138,9 @@ public class CalendarEvent {
Objects.equals(this.getCalName(), e.getCalName()) &&
Objects.equals(this.getCalAccountName(), e.getCalAccountName()) &&
(this.getColor() == e.getColor()) &&
(this.isAllDay() == e.isAllDay());
(this.isAllDay() == e.isAllDay()) &&
Objects.equals(this.getOrganizer(), e.getOrganizer()) &&
Objects.equals(this.getRemindersAbsoluteTs(), e.getRemindersAbsoluteTs());
} else {
return false;
}
@ -139,6 +158,8 @@ public class CalendarEvent {
result = 31 * result + Objects.hash(calAccountName);
result = 31 * result + Integer.valueOf(color).hashCode();
result = 31 * result + Boolean.valueOf(allDay).hashCode();
result = 31 * result + Objects.hash(organizer);
result = 31 * result + Objects.hash(remindersAbsoluteTs);
return result;
}
}

View File

@ -36,7 +36,6 @@ import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -60,10 +59,12 @@ public class CalendarManager {
Instances.TITLE,
Instances.DESCRIPTION,
Instances.EVENT_LOCATION,
Instances.ORGANIZER,
Instances.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
Instances.CALENDAR_COLOR,
Instances.ALL_DAY
Instances.ALL_DAY,
Instances.EVENT_ID //needed for reminders
};
private static final int lookahead_days = 7;
@ -98,26 +99,54 @@ public class CalendarManager {
return calendarEventList;
}
while (evtCursor.moveToNext()) {
long start = evtCursor.getLong(1);
long end = evtCursor.getLong(2);
long start = evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.BEGIN));
long end = evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.END));
if (end == 0) {
LOG.info("no end time, will parse duration string");
Time time = new Time(); //FIXME: deprecated FTW
time.parse(evtCursor.getString(3));
time.parse(evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.DURATION)));
end = start + time.toMillis(false);
}
CalendarEvent calEvent = new CalendarEvent(
start,
end,
evtCursor.getLong(0),
evtCursor.getString(4),
evtCursor.getString(5),
evtCursor.getString(6),
evtCursor.getString(7),
evtCursor.getString(8),
evtCursor.getInt(9),
!evtCursor.getString(10).equals("0")
evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances._ID)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.TITLE)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.DESCRIPTION)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.EVENT_LOCATION)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.CALENDAR_DISPLAY_NAME)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(CalendarContract.Calendars.ACCOUNT_NAME)),
evtCursor.getInt(evtCursor.getColumnIndexOrThrow(Instances.CALENDAR_COLOR)),
!evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.ALL_DAY)).equals("0"),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.ORGANIZER))
);
// Query reminders for this event
final Cursor reminderCursor = mContext.getContentResolver().query(
CalendarContract.Reminders.CONTENT_URI,
null,
CalendarContract.Reminders.EVENT_ID + " = ?",
new String[]{String.valueOf(evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.EVENT_ID)))},
null
);
if (reminderCursor != null && reminderCursor.getCount() > 0) {
final List<Long> reminders = new ArrayList<>();
while (reminderCursor.moveToNext()) {
int minutes = reminderCursor.getInt(reminderCursor.getColumnIndexOrThrow(CalendarContract.Reminders.MINUTES));
int method = reminderCursor.getInt(reminderCursor.getColumnIndexOrThrow(CalendarContract.Reminders.METHOD));
LOG.debug("Reminder Method: {}, Minutes: {}", method, minutes);
if (method == 1) //METHOD_ALERT
reminders.add(calEvent.getBegin() - minutes * 60 * 1000L);
}
reminderCursor.close();
calEvent.setRemindersAbsoluteTs(reminders);
}
if (!calendarIsBlacklisted(calEvent.getUniqueCalName())) {
calendarEventList.add(calEvent);
} else {

View File

@ -11,19 +11,38 @@ message CalendarService {
message CalendarServiceRequest {
optional uint32 begin = 1;
optional uint32 end = 2;
optional bool include_organizer = 3 [default = false];
optional bool include_title = 4 [default = true];
optional bool include_location = 5 [default = true];
optional bool include_description = 6 [default = false];
optional bool include_start_date = 7 [default = true];
optional bool include_end_date = 8 [default = false];
optional bool include_all_day = 9 [default = false];
optional uint32 max_organizer_length = 10;
optional uint32 max_title_length = 11;
optional uint32 max_location_length = 12;
optional uint32 max_description_length = 13;
optional uint32 max_events = 14;
}
message CalendarServiceResponse {
optional uint32 unknown = 1;
enum ResponseStatus {
UNKNOWN_RESPONSE_STATUS = 0;
OK = 1;
INVALID_DATE_RANGE = 2;
}
optional ResponseStatus status = 1;
repeated CalendarEvent calendar_event = 2;
}
message CalendarEvent {
optional string organizer = 1;
optional string title = 2;
optional string location = 3 [default = ""];
optional string description = 4 [default = ""];
optional uint32 begin = 5;
optional uint32 end = 6;
optional uint32 start_date = 5;
optional uint32 end_date = 6;
optional bool all_day = 7;
repeated uint32 reminder_time_in_secs = 8;
}
}

View File

@ -70,12 +70,11 @@ message CoreService {
optional Status status = 1;
repeated Requested requests = 2;
enum Status {
OK = 1;
UNAVAILABLE = 2;
UNKNOWN3 = 3;
UNKNOWN4 = 4;
OK = 1;
UNAVAILABLE = 2;
UNKNOWN3 = 3;
UNKNOWN4 = 4;
}
message Requested {
@ -83,8 +82,8 @@ message CoreService {
optional RequestedStatus status = 2;
enum RequestedStatus {
OK = 1;
KO = 2;
OK = 1;
KO = 2;
}
}

View File

@ -1483,6 +1483,7 @@
<string name="devicetype_garmin_vivomove_hr">Garmin Vivomove HR</string>
<string name="devicetype_garmin_vivomove_style">VĂ­vomove Style</string>
<string name="devicetype_garmin_instinct_2s">Garmin Instinct 2S</string>
<string name="devicetype_garmin_instinct_2_solar">Garmin Instinct 2 Solar</string>
<string name="devicetype_garmin_forerunner_245">Garmin Forerunner 245</string>
<string name="devicetype_garmin_vivoactive_4s">VĂ­voactive 4S</string>
<string name="devicetype_garmin_vivoactive_5">Vivoactive 5</string>
@ -2801,4 +2802,24 @@
<string name="pref_summary_bottom_navigation_bar_off">Switch between main screens only using horizontal swiping</string>
<string name="pref_summary_garmin_default_reply_suffix">Appended in addition to the suffix set in Gadgetbridge</string>
<string name="pref_title_garmin_default_reply_suffix">Use predefined reply suffix</string>
<string name="pref_dashboard_widget_today_upside_down_title">Midnight at bottom</string>
<string name="pref_dashboard_widget_today_upside_down_summary">In 24h mode, draw midnight at the bottom, midday at the top of the chart</string>
<string name="sleepasandroid_settings">Sleep As Android</string>
<string name="pref_sleepasandroid_enable_summary">Enable Sleep As Android integration</string>
<string name="pref_sleepasandroid_device_title">Provider device</string>
<string name="pref_sleepasandroid_device_summary">Select device as Sleep As Android data provider</string>
<string name="pref_sleepasandroid_features_title">Features</string>
<string name="pref_sleepasandroid_features_summary">Support differs from device to device</string>
<string name="pref_sleepasandroid_feat_alarms">Alarms</string>
<string name="pref_sleepasandroid_slot_title">Alarms slot</string>
<string name="pref_sleepasandroid_slot_summary">Which alarm slot to use when setting alarms</string>
<string name="alarm_slot_reset">Alarm slot has been set to default</string>
<string name="pref_sleepasandroid_feat_notifications">Notifications</string>
<string name="pref_sleepasandroid_feat_movement">Accelerometer</string>
<string name="pref_sleepasandroid_feat_heartrate">Heart rate</string>
<string name="pref_sleepasandroid_feat_oximetry">Oximetry</string>
<string name="pref_sleepasandroid_feat_spo2">SPO2</string>
<string name="pref_title_huawei_account">Huawei Account</string>
<string name="pref_summary_huawei_account">Huawei account used in pairing process. Setting it allows to pair without factory reset.</string>
</resources>

View File

@ -46,6 +46,14 @@
android:title="@string/pref_dashboard_widget_today_24h_title"
android:summary="@string/pref_dashboard_widget_today_24h_summary"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="dashboard_widget_today_24h_upside_down"
android:dependency="dashboard_widget_today_24h"
android:layout="@layout/preference_checkbox"
android:title="@string/pref_dashboard_widget_today_upside_down_title"
android:summary="@string/pref_dashboard_widget_today_upside_down_summary"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="dashboard_widget_today_2columns"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<EditTextPreference
android:icon="@drawable/ic_vpn_key"
android:key="huawei_account"
android:maxLength="17"
android:summary="@string/pref_summary_huawei_account"
android:title="@string/pref_title_huawei_account" />
</androidx.preference.PreferenceScreen>

View File

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

View File

@ -63,6 +63,11 @@
android:title="@string/bottom_nav_dashboard"
app:iconSpaceReserved="false" />
<Preference
android:key="pref_category_sleepasandroid"
android:title="@string/sleepasandroid_settings"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:key="pref_screen_theme"
android:title="@string/pref_title_theme"

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="pref_key_sleepasandroid_general"
android:title="@string/pref_header_general"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_key_sleepasandroid_enable"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_sleepasandroid_enable_summary"
android:title="@string/function_enabled"
app:iconSpaceReserved="false" />
<ListPreference
android:key="sleepasandroid_device"
android:entries="@array/empty_array"
android:entryValues="@array/empty_array"
android:title="@string/pref_sleepasandroid_device_title"
android:summary="@string/pref_sleepasandroid_device_summary"
app:iconSpaceReserved="false" />
<ListPreference
android:key="sleepasandroid_alarm_slot"
android:entries="@array/empty_array"
android:entryValues="@array/empty_array"
android:title="@string/pref_sleepasandroid_slot_title"
android:summary="@string/pref_sleepasandroid_slot_summary"
android:enabled="false"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_sleepasandroid_features"
android:title="@string/pref_sleepasandroid_features_title"
android:summary="@string/pref_sleepasandroid_features_summary"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
android:key="pref_key_sleepasandroid_feat_alarms"
android:title="@string/pref_sleepasandroid_feat_alarms"
app:iconSpaceReserved="false"
android:defaultValue="true">
</SwitchPreferenceCompat>
<SwitchPreferenceCompat
android:key="pref_key_sleepasandroid_feat_notifications"
android:title="@string/pref_sleepasandroid_feat_notifications"
app:iconSpaceReserved="false"
android:defaultValue="true">
</SwitchPreferenceCompat>
<SwitchPreferenceCompat
android:key="pref_key_sleepasandroid_feat_movement"
android:title="@string/pref_sleepasandroid_feat_movement"
app:iconSpaceReserved="false"
android:defaultValue="true"
android:enabled="false">
</SwitchPreferenceCompat>
<SwitchPreferenceCompat
android:key="pref_key_sleepasandroid_feat_hr"
android:title="@string/pref_sleepasandroid_feat_heartrate"
app:iconSpaceReserved="false"
android:defaultValue="true"
android:enabled="false">
</SwitchPreferenceCompat>
<SwitchPreferenceCompat
android:key="pref_key_sleepasandroid_feat_oximetry"
android:title="@string/pref_sleepasandroid_feat_oximetry"
app:iconSpaceReserved="false"
android:defaultValue="true"
android:enabled="false">
</SwitchPreferenceCompat>
<SwitchPreferenceCompat
android:key="pref_key_sleepasandroid_feat_spo2"
android:title="@string/pref_sleepasandroid_feat_spo2"
app:iconSpaceReserved="false"
android:defaultValue="true"
android:enabled="false">
</SwitchPreferenceCompat>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -25,22 +25,25 @@ public class CalendarEventTest extends TestBase {
@Test
public void testHashCode() {
CalendarEvent c1 =
new CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c2 =
new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c3 =
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c4 =
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, "some");
assertEquals(c1.hashCode(), c1.hashCode());
assertNotEquals(c1.hashCode(), c2.hashCode());
assertNotEquals(c2.hashCode(), c3.hashCode());
assertNotEquals(c3.hashCode(), c4.hashCode());
}
@Test
public void testSync() {
List<CalendarEvent> eventList = new ArrayList<>();
eventList.add(new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
eventList.add(new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null));
GBDevice dummyGBDevice = createDummyGDevice("00:00:01:00:03");
dummyGBDevice.setState(GBDevice.State.INITIALIZED);
@ -49,7 +52,7 @@ public class CalendarEventTest extends TestBase {
testCR.syncCalendar(eventList);
eventList.add(new CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
eventList.add(new CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null));
testCR.syncCalendar(eventList);
CalendarSyncStateDao calendarSyncStateDao = daoSession.getCalendarSyncStateDao();