Compare commits
51 Commits
2a0c2f4d90
...
ff51fa7309
Author | SHA1 | Date |
---|---|---|
José Rebelo | ff51fa7309 | |
José Rebelo | 81ea814681 | |
Daniele Gobbetti | 2605dff105 | |
Daniele Gobbetti | 39aea72ef7 | |
a0z | 60c3834e0b | |
Daniele Gobbetti | 6e0d7edfa7 | |
José Rebelo | 025f15e0fa | |
José Rebelo | db378faf47 | |
José Rebelo | 7bf73c6b41 | |
José Rebelo | f6754df211 | |
Daniele Gobbetti | b4414f0799 | |
José Rebelo | ce0a61e211 | |
José Rebelo | 3386e86158 | |
Daniele Gobbetti | c5a94d2927 | |
kuhy | 18a6a6b1c7 | |
Daniele Gobbetti | f1c7c97558 | |
myxor | 3baeb2b14e | |
Daniele Gobbetti | d6ade723d3 | |
Daniele Gobbetti | 863c1a5657 | |
Daniele Gobbetti | 16f5890a95 | |
hrdl | 10c5286ec1 | |
Daniele Gobbetti | 6b821a2f1f | |
José Rebelo | bcfaf7b3e8 | |
José Rebelo | 2f2b95beee | |
José Rebelo | 6569cd74ba | |
José Rebelo | 49d4792677 | |
José Rebelo | 418bb7d37a | |
José Rebelo | 044ac1e917 | |
José Rebelo | bad5cd4045 | |
Daniele Gobbetti | b3da377b34 | |
Daniele Gobbetti | 790019fa5a | |
Daniele Gobbetti | c96d1da1e5 | |
Daniele Gobbetti | 293449b5e0 | |
Daniele Gobbetti | 57db0c7c33 | |
Daniele Gobbetti | 379b8912cb | |
Daniele Gobbetti | 2aa8667998 | |
Daniele Gobbetti | 3a3ff5bc6a | |
Daniele Gobbetti | eff233d93a | |
Daniele Gobbetti | e73d4a7130 | |
Daniele Gobbetti | 40d064cf7f | |
Daniele Gobbetti | 719a104811 | |
Daniele Gobbetti | fb3338f099 | |
Daniele Gobbetti | fe1f610546 | |
Daniele Gobbetti | 1d1c6146a7 | |
José Rebelo | 500e930237 | |
José Rebelo | 3799ffb72c | |
José Rebelo | 13d6c49bb5 | |
Vitaliy Tomin | 67cf9b2f00 | |
Daniele Gobbetti | 173e2d29b0 | |
Marcel Alexandru Nitan | 2190c82ed7 | |
Arjan Schrijver | f186053dab |
|
@ -1,2 +0,0 @@
|
|||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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]));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||
|
||||
|
||||
public enum SleepAsAndroidFeature {
|
||||
HEART_RATE,
|
||||
ALARMS,
|
||||
NOTIFICATIONS,
|
||||
ACCELEROMETER,
|
||||
OXIMETRY,
|
||||
SPO2
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@ public class NotificationSubscriptionStatusMessage extends GFDIStatusMessage {
|
|||
}
|
||||
|
||||
public enum NotificationStatus {
|
||||
OK,
|
||||
ENABLED,
|
||||
DISABLED
|
||||
;
|
||||
|
||||
public static NotificationStatus fromId(int id) {
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue