diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs
deleted file mode 100644
index e8895216f..000000000
--- a/.settings/org.eclipse.buildship.core.prefs
+++ /dev/null
@@ -1,2 +0,0 @@
-connection.project.dir=
-eclipse.preferences.version=1
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 95f5ea8f3..b3736f4aa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -110,6 +110,24 @@
android:requestLegacyExternalStorage="true"
android:theme="@style/GadgetbridgeTheme"
tools:replace="android:label">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
index 4cb76457c..b2cac37b1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
@@ -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");
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SleepAsAndroidPreferencesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SleepAsAndroidPreferencesActivity.java
new file mode 100644
index 000000000..438f9181d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SleepAsAndroidPreferencesActivity.java
@@ -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 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 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 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 devices = GBApplication.app().getDeviceManager().getDevices();
+ List deviceMACs = new ArrayList<>();
+ List 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]));
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java
index 0b7ead89d..6bb683604 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java
@@ -843,7 +843,6 @@ public class GBDeviceAdapterv2 extends ListAdapter getSleepAsAndroidFeatures() {
+ return Collections.emptySet();
+ }
+
@Override
public int[] getSupportedDeviceSpecificConnectionSettings() {
int[] settings = new int[0];
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
index 1e57bfe38..1de36fa03 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
@@ -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 getSleepAsAndroidFeatures();
+
/**
* Returns device specific settings related to connection
*
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
index 8b5af212c..07be6adef 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
@@ -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);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SleepAsAndroidFeature.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SleepAsAndroidFeature.java
new file mode 100644
index 000000000..8092350fb
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SleepAsAndroidFeature.java
@@ -0,0 +1,11 @@
+package nodomain.freeyourgadget.gadgetbridge.devices;
+
+
+public enum SleepAsAndroidFeature {
+ HEART_RATE,
+ ALARMS,
+ NOTIFICATIONS,
+ ACCELEROMETER,
+ OXIMETRY,
+ SPO2
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java
index 3e80751a2..f7968bc78 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java
@@ -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 getSleepAsAndroidFeatures() {
+ return EnumSet.of(SleepAsAndroidFeature.ACCELEROMETER, SleepAsAndroidFeature.HEART_RATE, SleepAsAndroidFeature.ALARMS, SleepAsAndroidFeature.NOTIFICATIONS);
+ }
+
@Override
public int getWorldClocksSlotCount() {
return 20; // as enforced by Zepp
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/sleepasandroid/SleepAsAndroidAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/sleepasandroid/SleepAsAndroidAction.java
new file mode 100644
index 000000000..b63daf43d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/sleepasandroid/SleepAsAndroidAction.java
@@ -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";
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/sleepasandroid/SleepAsAndroidReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/sleepasandroid/SleepAsAndroidReceiver.java
new file mode 100644
index 000000000..61974d150
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/sleepasandroid/SleepAsAndroidReceiver.java
@@ -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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
index 30e3e76e8..cdf79dfda 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
@@ -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;
@@ -547,4 +547,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);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
index 758f0e441..8b5018662 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
@@ -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";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
index 5eefbe371..736366c7e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
@@ -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) {
+
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
index f3f2c5596..a2f9117b5 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
@@ -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.sleepasandroid.SleepAsAndroidAction;
+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;
@@ -145,6 +147,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
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);
+ }
}
}
@@ -254,6 +267,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private OsmandEventReceiver mOsmandAidlHelper = null;
+ private SleepAsAndroidReceiver mSleepAsAndroidReceiver = null;
+
private HashMap deviceLastScannedTimestamps = new HashMap<>();
private final String[] mMusicActions = {
@@ -1069,6 +1084,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;
}
}
@@ -1355,6 +1377,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();
@@ -1422,6 +1452,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mGenericWeatherReceiver);
mGenericWeatherReceiver = null;
}
+ if (mSleepAsAndroidReceiver != null) {
+ unregisterReceiver(mSleepAsAndroidReceiver);
+ mSleepAsAndroidReceiver = null;
+ }
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
index be5746a8d..23dbe0e98 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
@@ -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);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/SleepAsAndroidSender.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/SleepAsAndroidSender.java
new file mode 100644
index 000000000..ec3762775
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/SleepAsAndroidSender.java
@@ -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 hrData = new ArrayList<>();
+
+ private ArrayList accData = new ArrayList<>();
+ private ScheduledExecutorService accDataScheduler;
+ private Set 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 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.
+ * ...
+ */
+ public synchronized void sendExtra(Float hr, Long extraDataTimestamp, Long extraDataFramerate, ArrayList extraDataRR, ArrayList spo2Batch, Float sdnn, ArrayList 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.
+ * ...
+ */
+ 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 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;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java
index c1aeb8346..80ce3d4c6 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java
@@ -30,6 +30,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.Location;
import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
@@ -63,7 +64,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;
@@ -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 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) {
@@ -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?
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java
index 2698f6b77..dfd3000a0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java
@@ -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 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);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java
index 0900e19c3..17d8c7303 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java
@@ -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;
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f907d1705..29e75c227 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2796,4 +2796,20 @@
Switch between main screens only using horizontal swiping
Midnight at bottom
In 24h mode, draw midnight at the bottom, midday at the top of the chart
+
+ Sleep As Android
+ Enable Sleep As Android integration
+ Provider device
+ Select device as Sleep As Android data provider
+ Features
+ Support differs from device to device
+ Alarms
+ Alarms slot
+ Which alarm slot to use when setting alarms
+ Alarm slot has been set to default
+ Notifications
+ Accelerometer
+ Heart rate
+ Oximetry
+ SPO2
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 40ae1f9e5..c05ec9fd7 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -63,6 +63,11 @@
android:title="@string/bottom_nav_dashboard"
app:iconSpaceReserved="false" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file