mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-24 01:25:50 +01: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:
parent
f186053dab
commit
2190c82ed7
@ -1,2 +0,0 @@
|
||||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
@ -110,6 +110,24 @@
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/GadgetbridgeTheme"
|
||||
tools:replace="android:label">
|
||||
<receiver
|
||||
android:name=".externalevents.sleepasandroid.SleepAsAndroidReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="com.urbandroid.sleep.watch.START_TRACKING" />
|
||||
<action android:name="com.urbandroid.sleep.watch.STOP_TRACKING" />
|
||||
<action android:name="com.urbandroid.sleep.watch.SET_PAUSE" />
|
||||
<action android:name="com.urbandroid.sleep.watch.SET_SUSPENDED" />
|
||||
<action android:name="com.urbandroid.sleep.watch.SET_BATCH_SIZE" />
|
||||
<action android:name="com.urbandroid.sleep.watch.START_ALARM" />
|
||||
<action android:name="com.urbandroid.sleep.watch.STOP_ALARM" />
|
||||
<action android:name="com.urbandroid.sleep.watch.UPDATE_ALARM" />
|
||||
<action android:name="com.urbandroid.sleep.watch.SHOW_NOTIFICATION" />
|
||||
<action android:name="com.urbandroid.sleep.watch.HINT" />
|
||||
<action android:name="com.urbandroid.sleep.watch.CHECK_CONNECTED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<activity
|
||||
android:name=".activities.ControlCenterv2"
|
||||
android:label="@string/title_activity_controlcenter"
|
||||
@ -132,6 +150,10 @@
|
||||
android:name=".activities.DashboardPreferencesActivity"
|
||||
android:label="@string/dashboard_settings"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.SleepAsAndroidPreferencesActivity"
|
||||
android:label="@string/sleepasandroid_settings"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.AboutUserPreferencesActivity"
|
||||
android:label="@string/activity_prefs_about_you"
|
||||
|
@ -421,6 +421,15 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
});
|
||||
}
|
||||
|
||||
pref = findPreference("pref_category_sleepasandroid");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceClickListener(preference -> {
|
||||
Intent enableIntent = new Intent(requireContext(), SleepAsAndroidPreferencesActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
final Preference theme = findPreference("pref_key_theme");
|
||||
final Preference amoled_black = findPreference("pref_key_theme_amoled_black");
|
||||
|
||||
|
@ -0,0 +1,138 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SleepAsAndroidFeature;
|
||||
|
||||
public class SleepAsAndroidPreferencesActivity extends AbstractSettingsActivityV2 {
|
||||
@Override
|
||||
protected String fragmentTag() {
|
||||
return SleepAsAndroidPreferencesFragment.FRAGMENT_TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PreferenceFragmentCompat newFragment() {
|
||||
return new SleepAsAndroidPreferencesFragment();
|
||||
}
|
||||
|
||||
public static class SleepAsAndroidPreferencesFragment extends AbstractPreferenceFragment {
|
||||
static final String FRAGMENT_TAG = "SLEEPASANDROID_PREFERENCES_FRAGMENT";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||
setPreferencesFromResource(R.xml.sleepasandroid_preferences, rootKey);
|
||||
|
||||
final ListPreference sleepAsAndroidSlots = findPreference("sleepasandroid_alarm_slot");
|
||||
if (sleepAsAndroidSlots != null)
|
||||
{
|
||||
loadAlarmSlots(sleepAsAndroidSlots);
|
||||
}
|
||||
|
||||
final ListPreference sleepAsAndroidDevices = findPreference("sleepasandroid_device");
|
||||
if (sleepAsAndroidDevices != null) {
|
||||
loadDevicesList(sleepAsAndroidDevices);
|
||||
sleepAsAndroidDevices.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
GBDevice device = GBApplication.app().getDeviceManager().getDeviceByAddress(newValue.toString());
|
||||
if (device != null) {
|
||||
|
||||
GBApplication.getPrefs().getPreferences().edit().putString("sleepasandroid_device", device.getAddress()).apply();
|
||||
|
||||
Set<SleepAsAndroidFeature> supportedFeatures = device.getDeviceCoordinator().getSleepAsAndroidFeatures();
|
||||
findPreference("sleepasandroid_alarm_slot").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
|
||||
findPreference("pref_key_sleepasandroid_feat_alarms").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
|
||||
findPreference("pref_key_sleepasandroid_feat_notifications").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.NOTIFICATIONS));
|
||||
findPreference("pref_key_sleepasandroid_feat_movement").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ACCELEROMETER));
|
||||
findPreference("pref_key_sleepasandroid_feat_hr").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.HEART_RATE));
|
||||
findPreference("pref_key_sleepasandroid_feat_oximetry").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.OXIMETRY));
|
||||
findPreference("pref_key_sleepasandroid_feat_spo2").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.SPO2));
|
||||
|
||||
ListPreference alarmSlots = findPreference("sleepasandroid_alarm_slot");
|
||||
if (alarmSlots != null)
|
||||
{
|
||||
loadAlarmSlots(alarmSlots);
|
||||
if (alarmSlots.getEntries().length > 0)
|
||||
{
|
||||
alarmSlots.setValueIndex(0);
|
||||
GB.toast(getString(R.string.alarm_slot_reset), Toast.LENGTH_SHORT, GB.WARN);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
String defaultDeviceAddr = GBApplication.getPrefs().getString("sleepasandroid_device", "");
|
||||
if (!defaultDeviceAddr.isEmpty()) {
|
||||
GBDevice device = GBApplication.app().getDeviceManager().getDeviceByAddress(defaultDeviceAddr);
|
||||
if (device != null) {
|
||||
|
||||
Set<SleepAsAndroidFeature> supportedFeatures = device.getDeviceCoordinator().getSleepAsAndroidFeatures();
|
||||
findPreference("sleepasandroid_alarm_slot").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
|
||||
findPreference("pref_key_sleepasandroid_feat_alarms").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ALARMS));
|
||||
findPreference("pref_key_sleepasandroid_feat_notifications").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.NOTIFICATIONS));
|
||||
findPreference("pref_key_sleepasandroid_feat_movement").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.ACCELEROMETER));
|
||||
findPreference("pref_key_sleepasandroid_feat_hr").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.HEART_RATE));
|
||||
findPreference("pref_key_sleepasandroid_feat_oximetry").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.OXIMETRY));
|
||||
findPreference("pref_key_sleepasandroid_feat_spo2").setEnabled(supportedFeatures.contains(SleepAsAndroidFeature.SPO2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadAlarmSlots(ListPreference sleepAsAndroidSlots) {
|
||||
if (sleepAsAndroidSlots != null) {
|
||||
String defaultDeviceAddr = GBApplication.getPrefs().getString("sleepasandroid_device", "");
|
||||
if (!defaultDeviceAddr.isEmpty()) {
|
||||
GBDevice device = GBApplication.app().getDeviceManager().getDeviceByAddress(defaultDeviceAddr);
|
||||
if (device != null) {
|
||||
int maxAlarmSlots = device.getDeviceCoordinator().getAlarmSlotCount(device);
|
||||
if (maxAlarmSlots > 0) {
|
||||
List<String> alarmSlots = new ArrayList<>();
|
||||
int reservedAlarmSlots = GBApplication.getPrefs().getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_ALARMS_CALENDAR, 0);
|
||||
for (int i = reservedAlarmSlots + 1;i < maxAlarmSlots; i++) {
|
||||
alarmSlots.add(String.valueOf(i));
|
||||
}
|
||||
sleepAsAndroidSlots.setEntryValues(alarmSlots.toArray(new String[0]));
|
||||
sleepAsAndroidSlots.setEntries(alarmSlots.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadDevicesList(ListPreference sleepAsAndroidDevices) {
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
List<String> deviceMACs = new ArrayList<>();
|
||||
List<String> deviceNames = new ArrayList<>();
|
||||
for (GBDevice dev : devices) {
|
||||
if (dev.getDeviceCoordinator().supportsSleepAsAndroid()) {
|
||||
deviceMACs.add(dev.getAddress());
|
||||
deviceNames.add(dev.getAliasOrName());
|
||||
}
|
||||
}
|
||||
|
||||
sleepAsAndroidDevices.setEntryValues(deviceMACs.toArray(new String[0]));
|
||||
sleepAsAndroidDevices.setEntries(deviceNames.toArray(new String[0]));
|
||||
}
|
||||
}
|
@ -843,7 +843,6 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
|
||||
|
||||
private void showDeviceSubmenu(final View v, final GBDevice device) {
|
||||
boolean deviceConnected = device.getState() != GBDevice.State.NOT_CONNECTED;
|
||||
|
||||
PopupMenu menu = new PopupMenu(v.getContext(), v);
|
||||
menu.inflate(R.menu.fragment_devices_device_submenu);
|
||||
|
||||
|
@ -78,6 +78,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
@ -586,6 +587,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSleepAsAndroid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SleepAsAndroidFeature> getSleepAsAndroidFeatures() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificConnectionSettings() {
|
||||
int[] settings = new int[0];
|
||||
|
@ -29,6 +29,7 @@ import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
@ -57,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
|
||||
|
||||
/**
|
||||
* This interface is implemented at least once for every supported gadget device.
|
||||
@ -511,6 +513,11 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
boolean supportsMusicInfo();
|
||||
|
||||
/**
|
||||
* Indicates whether the device supports features required by Sleep As Android
|
||||
*/
|
||||
boolean supportsSleepAsAndroid();
|
||||
|
||||
/**
|
||||
* Indicates the maximum reminder message length.
|
||||
*/
|
||||
@ -569,6 +576,12 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
boolean supportsUnicodeEmojis();
|
||||
|
||||
/**
|
||||
* Returns the set of supported sleep as Android features
|
||||
* @return Set
|
||||
*/
|
||||
Set<SleepAsAndroidFeature> getSleepAsAndroidFeatures();
|
||||
|
||||
/**
|
||||
* Returns device specific settings related to connection
|
||||
*
|
||||
|
@ -20,6 +20,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
@ -148,4 +149,6 @@ public interface EventHandler {
|
||||
void onPowerOff();
|
||||
|
||||
void onSetGpsLocation(Location location);
|
||||
|
||||
void onSleepAsAndroidAction(String action, Bundle extras);
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||
|
||||
|
||||
public enum SleepAsAndroidFeature {
|
||||
HEART_RATE,
|
||||
ALARMS,
|
||||
NOTIFICATIONS,
|
||||
ACCELEROMETER,
|
||||
OXIMETRY,
|
||||
SPO2
|
||||
}
|
@ -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
|
||||
|
@ -0,0 +1,16 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid;
|
||||
|
||||
public class SleepAsAndroidAction {
|
||||
public static final String START_TRACKING = "com.urbandroid.sleep.watch.START_TRACKING";
|
||||
public static final String STOP_TRACKING = "com.urbandroid.sleep.watch.STOP_TRACKING";
|
||||
public static final String SET_PAUSE = "com.urbandroid.sleep.watch.SET_PAUSE";
|
||||
public static final String SET_SUSPENDED = "com.urbandroid.sleep.watch.SET_SUSPENDED";
|
||||
public static final String SET_BATCH_SIZE = "com.urbandroid.sleep.watch.SET_BATCH_SIZE";
|
||||
public static final String START_ALARM = "com.urbandroid.sleep.watch.START_ALARM";
|
||||
public static final String STOP_ALARM = "com.urbandroid.sleep.watch.STOP_ALARM";
|
||||
public static final String UPDATE_ALARM = "com.urbandroid.sleep.watch.UPDATE_ALARM";
|
||||
public static final String SHOW_NOTIFICATION = "com.urbandroid.sleep.watch.SHOW_NOTIFICATION";
|
||||
public static final String HINT = "com.urbandroid.sleep.watch.HINT";
|
||||
public static final String CHECK_CONNECTED = "com.urbandroid.sleep.watch.CHECK_CONNECTED";
|
||||
public static final String CONFIRM_CONNECTED = "com.urbandroid.sleep.watch.CONFIRM_CONNECTED";
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
|
||||
public class SleepAsAndroidReceiver extends BroadcastReceiver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SleepAsAndroidReceiver.class);
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_enable", false)) {
|
||||
GBApplication.deviceService().onSleepAsAndroidAction(action, intent.getExtras());
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcelable;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -32,6 +32,7 @@ import android.graphics.BitmapFactory;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.SmsManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
@ -1181,4 +1182,9 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSleepAsAndroidAction(String action, Bundle extras) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,8 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.SilentModeReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGermanyReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,12 +22,12 @@ import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -512,4 +512,12 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
}
|
||||
delegate.onSetGpsLocation(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSleepAsAndroidAction(String action, Bundle extras) {
|
||||
if (checkBusy("sleep as android")) {
|
||||
return;
|
||||
}
|
||||
delegate.onSleepAsAndroidAction(action, extras);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,573 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SleepAsAndroidFeature;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class SleepAsAndroidSender {
|
||||
|
||||
private final Logger LOG = LoggerFactory.getLogger(SleepAsAndroidSender.class);
|
||||
private final String PACKAGE_SLEEP_AS_ANDROID = "com.urbandroid.sleep";
|
||||
private final String ACTION_EXTRA_DATA_UPDATE = "com.urbandroid.sleep.ACTION_EXTRA_DATA_UPDATE";
|
||||
private final String ACTION_MOVEMENT_DATA_UPDATE = "com.urbandroid.sleep.watch.DATA_UPDATE";
|
||||
private final String ACTION_HEART_RATE_DATA_UPDATE = "com.urbandroid.sleep.watch.HR_DATA_UPDATE";
|
||||
private final String ACTION_RESUME_FROM_WATCH = "com.urbandroid.sleep.watch.RESUME_FROM_WATCH";
|
||||
private final String ACTION_PAUSE_FROM_WATCH = "com.urbandroid.sleep.watch.PAUSE_FROM_WATCH";
|
||||
private final String ACTION_SNOOZE_FROM_WATCH = "com.urbandroid.sleep.watch.SNOOZE_FROM_WATCH";
|
||||
private final String ACTION_DISMISS_FROM_WATCH = "com.urbandroid.sleep.watch.DISMISS_FROM_WATCH";
|
||||
|
||||
private final String MAX_RAW_DATA = "MAX_RAW_DATA";
|
||||
private final String DATA = "DATA";
|
||||
private final String EXTRA_DATA_HR = "com.urbandroid.sleep.EXTRA_DATA_HR";
|
||||
private final String EXTRA_DATA_RR = "com.urbandroid.sleep.EXTRA_DATA_RR";
|
||||
private final String EXTRA_DATA_SPO2 = "com.urbandroid.sleep.EXTRA_DATA_SPO2";
|
||||
private final String EXTRA_DATA_SDNN = "com.urbandroid.sleep.EXTRA_DATA_SDNN";
|
||||
private final String EXTRA_DATA_TIMESTAMP = "com.urbandroid.sleep.EXTRA_DATA_TIMESTAMP";
|
||||
private final String EXTRA_DATA_FRAMERATE = "com.urbandroid.sleep.EXTRA_DATA_FRAMERATE";
|
||||
private final String EXTRA_DATA_BATCH = "com.urbandroid.sleep.EXTRA_DATA_BATCH";
|
||||
|
||||
|
||||
private GBDevice device;
|
||||
private boolean trackingOngoing = false;
|
||||
private boolean trackingPaused = false;
|
||||
|
||||
private ScheduledExecutorService trackingPauseScheduler;
|
||||
private long batchSize = 1;
|
||||
private long lastRawDataMs = 0;
|
||||
private float maxRawData = 0;
|
||||
private long lastHrDataMs = 0;
|
||||
private ArrayList<Float> hrData = new ArrayList<>();
|
||||
|
||||
private ArrayList<Float> accData = new ArrayList<>();
|
||||
private ScheduledExecutorService accDataScheduler;
|
||||
private Set<SleepAsAndroidFeature> features;
|
||||
|
||||
public SleepAsAndroidSender(GBDevice gbDevice) {
|
||||
this.device = gbDevice;
|
||||
this.features = gbDevice.getDeviceCoordinator().getSleepAsAndroidFeatures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a SleepAsAndroid feature is enabled
|
||||
*
|
||||
* @param feature the feature
|
||||
* @return true if the feature is enabled
|
||||
*/
|
||||
public boolean hasFeature(SleepAsAndroidFeature feature) {
|
||||
return this.features.contains(feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a SleepAsAndroid feature is enabled
|
||||
* @param feature
|
||||
* @return
|
||||
*/
|
||||
public boolean isFeatureEnabled(SleepAsAndroidFeature feature) {
|
||||
boolean enabled = isSleepAsAndroidEnabled();
|
||||
if (enabled) {
|
||||
switch (feature) {
|
||||
case ACCELEROMETER:
|
||||
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_movement", false);
|
||||
break;
|
||||
case HEART_RATE:
|
||||
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_hr", false);
|
||||
break;
|
||||
case SPO2:
|
||||
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_spo2", false);
|
||||
break;
|
||||
case OXIMETRY:
|
||||
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_oximetry", false);
|
||||
break;
|
||||
case NOTIFICATIONS:
|
||||
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_notifications", false);
|
||||
break;
|
||||
case ALARMS:
|
||||
enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_alarms", false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enabled features
|
||||
*
|
||||
* @return all enabled features
|
||||
*/
|
||||
public Set<SleepAsAndroidFeature> getFeatures() {
|
||||
return features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tracking
|
||||
*/
|
||||
public void startTracking() {
|
||||
if (!isDeviceDefault()) return;
|
||||
|
||||
stopTracking();
|
||||
|
||||
accDataScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
accDataScheduler.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
aggregateAndSendAccelData();
|
||||
}
|
||||
}, 9999, 9999, TimeUnit.MILLISECONDS);
|
||||
|
||||
lastRawDataMs = System.currentTimeMillis();
|
||||
lastHrDataMs = System.currentTimeMillis();
|
||||
|
||||
this.trackingOngoing = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking
|
||||
*/
|
||||
public void stopTracking() {
|
||||
if (!isDeviceDefault() || !trackingOngoing) return;
|
||||
if (accDataScheduler != null) {
|
||||
accDataScheduler.shutdownNow();
|
||||
accDataScheduler = null;
|
||||
}
|
||||
|
||||
this.trackingOngoing = false;
|
||||
this.hrData = new ArrayList<>();
|
||||
this.accData = new ArrayList<>();
|
||||
this.lastHrDataMs = 0;
|
||||
this.lastRawDataMs = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause tracking
|
||||
*
|
||||
* @param timeout the timeout in milliseconds before resuming
|
||||
*/
|
||||
public void pauseTracking(long timeout) {
|
||||
if (!isDeviceDefault() || !trackingOngoing) return;
|
||||
|
||||
if (timeout <= 0) {
|
||||
resumeTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
pauseTracking();
|
||||
trackingPauseScheduler = setPauseTracking(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #pauseTracking(long)} but pausing and resuming is controlled by a toggle
|
||||
*
|
||||
* @param suspended true if the tracking should be paused, false if should be resumed
|
||||
*/
|
||||
public void pauseTracking(boolean suspended) {
|
||||
if (!isDeviceDefault() || !trackingOngoing) return;
|
||||
trackingPaused = suspended;
|
||||
if (!trackingPaused) {
|
||||
resumeTracking();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a scheduler to resume tracking
|
||||
*
|
||||
* @param delay the delay
|
||||
* @return the scheduler
|
||||
*/
|
||||
private ScheduledExecutorService setPauseTracking(long delay) {
|
||||
trackingPaused = true;
|
||||
trackingPauseScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
trackingPauseScheduler.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
resumeTracking();
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
|
||||
return trackingPauseScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause tracking
|
||||
*/
|
||||
private void pauseTracking() {
|
||||
if (!isDeviceDefault() && trackingPaused) return;
|
||||
trackingPaused = true;
|
||||
stopTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume tracking
|
||||
*/
|
||||
private void resumeTracking() {
|
||||
if (!isDeviceDefault() && !trackingPaused) return;
|
||||
if (trackingPauseScheduler != null) {
|
||||
trackingPauseScheduler.shutdownNow();
|
||||
trackingPauseScheduler = null;
|
||||
}
|
||||
trackingPaused = false;
|
||||
startTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the batch size
|
||||
*
|
||||
* @param batchSize the batch size
|
||||
*/
|
||||
public void setBatchSize(long batchSize) {
|
||||
if (!isDeviceDefault()) return;
|
||||
LOG.debug("Setting batch size to " + batchSize);
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm that the device is connected
|
||||
*/
|
||||
public void confirmConnected() {
|
||||
if (!isDeviceDefault()) return;
|
||||
LOG.debug("Confirming connected");
|
||||
Intent intent = new Intent(SleepAsAndroidAction.CONFIRM_CONNECTED);
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* On accelerometer changed
|
||||
*
|
||||
* @param x the x value
|
||||
* @param y the y value
|
||||
* @param z the z value
|
||||
*/
|
||||
public void onAccelChanged(float x, float y, float z) {
|
||||
if (!isDeviceDefault() || !isFeatureEnabled(SleepAsAndroidFeature.ACCELEROMETER) || !hasFeature(SleepAsAndroidFeature.ACCELEROMETER) || !trackingOngoing)
|
||||
return;
|
||||
if (trackingPaused)
|
||||
return;
|
||||
|
||||
updateMaxRawData(x, y, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate and send the acceleration data
|
||||
*/
|
||||
private synchronized void aggregateAndSendAccelData() {
|
||||
if (!trackingOngoing || trackingPaused) return;
|
||||
if (maxRawData > 0) {
|
||||
accData.add(maxRawData);
|
||||
maxRawData = 0;
|
||||
if (accData.size() == batchSize) {
|
||||
sendAccelData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the acceleration data
|
||||
*/
|
||||
private void sendAccelData() {
|
||||
LOG.debug("Sending movement data: " + this.accData + " batch size: " + batchSize + " array size: " + accData.size());
|
||||
Intent intent = new Intent(ACTION_MOVEMENT_DATA_UPDATE);
|
||||
intent.putExtra(MAX_RAW_DATA, convertToFloatArray(this.accData));
|
||||
accData.clear();
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the max raw data
|
||||
* @param x the x value
|
||||
* @param y the y value
|
||||
* @param z the z value
|
||||
*/
|
||||
private void updateMaxRawData(float x, float y, float z) {
|
||||
float maxRaw = calculateAccelerationMagnitude(x, y, z);
|
||||
if (maxRaw > maxRawData) {
|
||||
maxRawData = maxRaw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the acceleration magnitude
|
||||
* @param x the x value
|
||||
* @param y the y value
|
||||
* @param z the z value
|
||||
* @return
|
||||
*/
|
||||
protected float calculateAccelerationMagnitude(float x, float y, float z) {
|
||||
double sqrt = Math.sqrt((x * x) + (y * y) + (z * z));
|
||||
return (float)sqrt;
|
||||
}
|
||||
|
||||
/**
|
||||
* On heart rate changed
|
||||
*
|
||||
* @param hr the heart rate
|
||||
* @param sendDelay the send delay in ms. If 0 the data will be send right away. Anything bigger will gather all the data then send it all after the specified interval
|
||||
*/
|
||||
public void onHrChanged(float hr, long sendDelay) {
|
||||
if (!isDeviceDefault() || !isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE) || !hasFeature(SleepAsAndroidFeature.HEART_RATE) || !trackingOngoing)
|
||||
return;
|
||||
if (trackingPaused) return;
|
||||
|
||||
updateLastHrData(hr);
|
||||
|
||||
if (lastHrDataMs == 0) {
|
||||
lastHrDataMs = System.currentTimeMillis();
|
||||
}
|
||||
long ms = System.currentTimeMillis();
|
||||
if (ms - lastHrDataMs >= sendDelay) {
|
||||
lastHrDataMs = ms;
|
||||
sendHrData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the heart rate data
|
||||
*/
|
||||
private synchronized void sendHrData() {
|
||||
LOG.debug("Sending heart rate data: " + this.hrData);
|
||||
Intent intent = new Intent(ACTION_HEART_RATE_DATA_UPDATE);
|
||||
intent.putExtra(DATA, convertToFloatArray(this.hrData));
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
this.hrData.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last heart rate data
|
||||
* @param hr the heart rate
|
||||
*/
|
||||
private void updateLastHrData(float hr) {
|
||||
this.hrData.add(hr);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a generic intent to carry data from various sensors.
|
||||
* See Sleep As Android documentation for parameters values.
|
||||
* <a href="https://docs.sleep.urbandroid.org/devs/wearable_api.html#send-various-body-sensors-data-hr-rr-spo2-sdnn">...</a>
|
||||
*/
|
||||
public synchronized void sendExtra(Float hr, Long extraDataTimestamp, Long extraDataFramerate, ArrayList<Float> extraDataRR, ArrayList<Float> spo2Batch, Float sdnn, ArrayList<Float> extraDataBatch) {
|
||||
|
||||
if (!isDeviceDefault() || !trackingOngoing) return;
|
||||
if (trackingPaused) return;
|
||||
Context context = GBApplication.getContext();
|
||||
Intent intent = new Intent(ACTION_EXTRA_DATA_UPDATE);
|
||||
|
||||
// Heart Rate
|
||||
if (hr != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
|
||||
intent.putExtra(EXTRA_DATA_HR, hr);
|
||||
}
|
||||
|
||||
// SpO2
|
||||
if (spo2Batch != null && (hasFeature(SleepAsAndroidFeature.SPO2) && isFeatureEnabled(SleepAsAndroidFeature.SPO2))) {
|
||||
intent.putExtra(EXTRA_DATA_SPO2, true);
|
||||
intent.putExtra(EXTRA_DATA_BATCH, convertToFloatArray(spo2Batch));
|
||||
}
|
||||
|
||||
// SDNN
|
||||
if (sdnn != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
|
||||
intent.putExtra(EXTRA_DATA_SDNN, sdnn);
|
||||
}
|
||||
|
||||
// RR Intervals
|
||||
if (extraDataRR != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
|
||||
intent.putExtra(EXTRA_DATA_RR, convertToFloatArray(extraDataRR));
|
||||
}
|
||||
|
||||
if (extraDataBatch != null) {
|
||||
if (isDeviceDefault()) {
|
||||
for (int i = 0; i < extraDataBatch.size(); i++) {
|
||||
extraDataBatch.set(i, 0.0f);
|
||||
}
|
||||
}
|
||||
intent.putExtra(EXTRA_DATA_BATCH, convertToFloatArray(extraDataBatch));
|
||||
}
|
||||
|
||||
if (extraDataTimestamp != null) {
|
||||
intent.putExtra(EXTRA_DATA_TIMESTAMP, extraDataTimestamp);
|
||||
}
|
||||
if (extraDataFramerate != null) {
|
||||
intent.putExtra(EXTRA_DATA_FRAMERATE, extraDataFramerate);
|
||||
}
|
||||
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a generic intent to carry data from various sensors.
|
||||
* See Sleep As Android documentation for parameters values.
|
||||
* <a href="https://docs.sleep.urbandroid.org/devs/wearable_api.html#send-various-body-sensors-data-hr-rr-spo2-sdnn">...</a>
|
||||
*/
|
||||
public void sendExtra(Float hr, Float extraDataRR, Float spo2, Float sdnn, Long sdnnTimestamp) {
|
||||
|
||||
if (!isDeviceDefault() || !trackingOngoing) return;
|
||||
if (trackingPaused) return;
|
||||
Intent intent = new Intent(ACTION_EXTRA_DATA_UPDATE);
|
||||
|
||||
// Heart Rate
|
||||
if (hr != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
|
||||
intent.putExtra(EXTRA_DATA_HR, hr);
|
||||
}
|
||||
|
||||
// SpO2
|
||||
if (spo2 != null && (hasFeature(SleepAsAndroidFeature.SPO2) && isFeatureEnabled(SleepAsAndroidFeature.SPO2))) {
|
||||
intent.putExtra(EXTRA_DATA_SPO2, spo2);
|
||||
}
|
||||
|
||||
// SDNN
|
||||
if (sdnn != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
|
||||
intent.putExtra(EXTRA_DATA_SDNN, sdnn);
|
||||
}
|
||||
|
||||
if (extraDataRR != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
|
||||
intent.putExtra(EXTRA_DATA_RR, extraDataRR);
|
||||
}
|
||||
|
||||
if (sdnnTimestamp != null) {
|
||||
intent.putExtra(EXTRA_DATA_TIMESTAMP, sdnnTimestamp);
|
||||
}
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds 5 minutes of tracking pause time
|
||||
*/
|
||||
public void sendPauseTracking() {
|
||||
if (!isDeviceDefault() || !trackingOngoing) return;
|
||||
LOG.debug("Sending pause");
|
||||
Intent intent = new Intent(ACTION_PAUSE_FROM_WATCH);
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume tracking
|
||||
*/
|
||||
public void sendResumeTracking() {
|
||||
if (!isDeviceDefault() || !trackingOngoing) return;
|
||||
LOG.debug("Sending resume");
|
||||
Intent intent = new Intent(ACTION_RESUME_FROM_WATCH);
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snooze the current alarm
|
||||
*/
|
||||
public void sendSnoozeAlarm() {
|
||||
if (!isDeviceDefault()) return;
|
||||
if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) return;
|
||||
LOG.debug("Sending snooze");
|
||||
Intent intent = new Intent(ACTION_SNOOZE_FROM_WATCH);
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the current alarm
|
||||
*/
|
||||
public void sendDismissAlarm() {
|
||||
if (!isDeviceDefault()) return;
|
||||
if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) return;
|
||||
LOG.debug("Sending dismiss");
|
||||
Intent intent = new Intent(ACTION_DISMISS_FROM_WATCH);
|
||||
broadcastToSleepAsAndroid(intent);
|
||||
}
|
||||
|
||||
private void broadcastToSleepAsAndroid(Intent intent) {
|
||||
if (!isDeviceDefault()) return;
|
||||
intent.setPackage(PACKAGE_SLEEP_AS_ANDROID);
|
||||
GBApplication.getContext().sendBroadcast(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the device is set as default provider for Sleep As Android
|
||||
*
|
||||
* @return true if the device is set as default
|
||||
*/
|
||||
public boolean isDeviceDefault() {
|
||||
if (device == null || !device.isInitialized()) return false;
|
||||
if (isSleepAsAndroidEnabled()) {
|
||||
return device.getAddress().equals(GBApplication.getPrefs().getString("sleepasandroid_device", ""));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSleepAsAndroidEnabled() {
|
||||
return GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_enable", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the device is allowed to receive a specific action
|
||||
* @param action the action send my the broadcast receiver
|
||||
*/
|
||||
public void validateAction(String action) {
|
||||
if (isDeviceDefault()) {
|
||||
switch (action) {
|
||||
case SleepAsAndroidAction.HINT:
|
||||
case SleepAsAndroidAction.SHOW_NOTIFICATION:
|
||||
if (!hasFeature(SleepAsAndroidFeature.NOTIFICATIONS) || !isFeatureEnabled(SleepAsAndroidFeature.NOTIFICATIONS)) {
|
||||
throw new UnsupportedOperationException("Action not valid");
|
||||
}
|
||||
break;
|
||||
case SleepAsAndroidAction.START_ALARM:
|
||||
case SleepAsAndroidAction.STOP_ALARM:
|
||||
case SleepAsAndroidAction.UPDATE_ALARM:
|
||||
if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) {
|
||||
throw new UnsupportedOperationException("Action not valid");
|
||||
}
|
||||
break;
|
||||
case SleepAsAndroidAction.START_TRACKING:
|
||||
case SleepAsAndroidAction.STOP_TRACKING:
|
||||
case SleepAsAndroidAction.SET_BATCH_SIZE:
|
||||
if (!hasFeature(SleepAsAndroidFeature.ACCELEROMETER) || !isFeatureEnabled(SleepAsAndroidFeature.ACCELEROMETER)) {
|
||||
throw new UnsupportedOperationException("Action not valid");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Action not valid");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ArrayList to a float array
|
||||
*
|
||||
* @param list the ArrayList
|
||||
* @return the float array
|
||||
*/
|
||||
private float[] convertToFloatArray(ArrayList<Float> list) {
|
||||
float[] result = new float[list.size()];
|
||||
int i = 0;
|
||||
for (float f : list) {
|
||||
result[i++] = f;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured alarm slot
|
||||
*
|
||||
* @return the alarm slot to be used for setting alarms on the watch
|
||||
*/
|
||||
public static int getAlarmSlot() {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
String slotString = prefs.getString("sleepasandroid_alarm_slot", "");
|
||||
if (!slotString.isEmpty()) {
|
||||
return Integer.parseInt(slotString);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -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?
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
78
app/src/main/res/xml/sleepasandroid_preferences.xml
Normal file
78
app/src/main/res/xml/sleepasandroid_preferences.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user