1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-09-27 08:47:03 +02:00

feature: Sleep as android support

Implement support for Sleep As Android with an usable example for ZeppOs
devices

Sleep as Android documentation:

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

Signed-off-by: Marcel Alexandru Nitan <nitan.marcel@protonmail.com>
This commit is contained in:
Marcel Alexandru Nitan 2024-04-06 15:21:39 +03:00
parent f186053dab
commit 2190c82ed7
24 changed files with 1180 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ import android.content.Intent;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.os.Parcelable;
import android.os.Bundle;
import android.provider.ContactsContract;
import java.util.ArrayList;
@ -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);
}
}

View File

@ -79,6 +79,9 @@ public interface DeviceService extends EventHandler {
String ACTION_SET_GPS_LOCATION = PREFIX + ".action.set_gps_location";
String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color";
String ACTION_POWER_OFF = PREFIX + ".action.power_off";
String ACTION_SLEEP_AS_ANDROID = ".action.sleep_as_android";
String EXTRA_SLEEP_AS_ANDROID_ACTION = "sleepasandroid_action";
String EXTRA_NOTIFICATION_BODY = "notification_body";
String EXTRA_NOTIFICATION_FLAGS = "notification_flags";
String EXTRA_NOTIFICATION_ID = "notification_id";

View File

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

View File

@ -82,6 +82,8 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.SilentModeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGermanyReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.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<String, Long> 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;
}
}
}

View File

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

View File

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

View File

@ -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<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) {
@ -2612,6 +2618,8 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
MiBand2SampleProvider provider = new MiBand2SampleProvider(gbDevice, session);
MiBandActivitySample sample = createActivitySample(device, user, ts, provider);
sample.setHeartRate(getHeartrateBpm());
sleepAsAndroidSender.onHrChanged(sample.getHeartRate(), 0);
// sample.setSteps(getSteps());
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
sample.setRawKind(HuamiConst.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that?

View File

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

View File

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

View File

@ -2796,4 +2796,20 @@
<string name="pref_summary_bottom_navigation_bar_off">Switch between main screens only using horizontal swiping</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>
</resources>

View File

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

View File

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