mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-27 20:36:51 +01:00
Xiaomi Smart Band 7: Initial support
This commit is contained in:
parent
dcce900f23
commit
ba565df088
@ -73,6 +73,7 @@ vendor's servers.
|
||||
- Mi
|
||||
- [Band, Band 1A, Band 1S](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band), [Band 2](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2), [Band 3](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3)
|
||||
- [Band 4](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4), [Band 5](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-5), [Band 6](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-6) [**\[!\]**](#special-pairing-procedures)
|
||||
- [Xiaomi Smart Band 7](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-7) [**\[!\]**](#special-pairing-procedures)
|
||||
- Scale 2 (Currently only displays a toast after stepping on the scale)
|
||||
- [MyKronoz ZeTime](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
|
||||
- NO.1 F1
|
||||
|
@ -264,6 +264,9 @@ dependencies {
|
||||
// JSR-310 timezones backport for Android, since we're still on java 7
|
||||
implementation 'com.jakewharton.threetenabp:threetenabp:1.4.0'
|
||||
testImplementation 'org.threeten:threetenbp:1.6.0'
|
||||
|
||||
// Android SDK bundles org.json, but we need an actual implementation to replace the stubs in tests
|
||||
testImplementation 'org.json:json:20180813'
|
||||
}
|
||||
|
||||
preBuild.dependsOn(":GBDaoGenerator:genSources")
|
||||
|
@ -74,6 +74,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.NotificationCollectorMonitorService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
@ -217,6 +218,8 @@ public class GBApplication extends Application {
|
||||
|
||||
setupExceptionHandler();
|
||||
|
||||
Weather.getInstance().setCacheFile(getCacheDir(), prefs.getBoolean("cache_weather", true));
|
||||
|
||||
deviceManager = new DeviceManager(this);
|
||||
String language = prefs.getString("language", "default");
|
||||
setLanguage(language);
|
||||
|
@ -84,7 +84,7 @@ public class ConfigureReminders extends AbstractGBActivity {
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
|
||||
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
int reservedSlots = prefs.getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR, 9);
|
||||
int reservedSlots = prefs.getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR, coordinator.supportsCalendarEvents() ? 0 : 9);
|
||||
|
||||
int deviceSlots = coordinator.getReminderSlotCount() - reservedSlots;
|
||||
|
||||
|
@ -76,6 +76,7 @@ import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.Widget;
|
||||
@ -100,6 +101,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@ -179,8 +182,22 @@ public class DebugActivity extends AbstractGBActivity {
|
||||
notificationSpec.body = testString;
|
||||
notificationSpec.sender = testString;
|
||||
notificationSpec.subject = testString;
|
||||
notificationSpec.sourceAppId = BuildConfig.APPLICATION_ID;
|
||||
notificationSpec.sourceName = getApplicationContext().getApplicationInfo()
|
||||
.loadLabel(getApplicationContext().getPackageManager())
|
||||
.toString();
|
||||
notificationSpec.type = NotificationType.sortedValues()[sendTypeSpinner.getSelectedItemPosition()];
|
||||
notificationSpec.pebbleColor = notificationSpec.type.color;
|
||||
notificationSpec.attachedActions = new ArrayList<>();
|
||||
|
||||
if (notificationSpec.type == NotificationType.GENERIC_SMS) {
|
||||
// REPLY action
|
||||
NotificationSpec.Action replyAction = new NotificationSpec.Action();
|
||||
replyAction.title = "Reply";
|
||||
replyAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR;
|
||||
notificationSpec.attachedActions.add(replyAction);
|
||||
}
|
||||
|
||||
GBApplication.deviceService().onNotification(notificationSpec);
|
||||
}
|
||||
});
|
||||
@ -303,6 +320,42 @@ public class DebugActivity extends AbstractGBActivity {
|
||||
}
|
||||
});
|
||||
|
||||
Button setWeatherButton = findViewById(R.id.setWeatherButton);
|
||||
setWeatherButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (Weather.getInstance().getWeatherSpec() == null) {
|
||||
final WeatherSpec weatherSpec = new WeatherSpec();
|
||||
weatherSpec.forecasts = new ArrayList<>();
|
||||
|
||||
weatherSpec.location = "Green Hill";
|
||||
weatherSpec.currentConditionCode = 601; // snow
|
||||
weatherSpec.currentCondition = Weather.getConditionString(weatherSpec.currentConditionCode);
|
||||
|
||||
weatherSpec.currentTemp = 15 + 273;
|
||||
weatherSpec.currentHumidity = 30;
|
||||
|
||||
weatherSpec.windSpeed = 10;
|
||||
weatherSpec.windDirection = 12;
|
||||
weatherSpec.timestamp = (int) (System.currentTimeMillis() / 1000);
|
||||
weatherSpec.todayMinTemp = 10 + 273;
|
||||
weatherSpec.todayMaxTemp = 25 + 273;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final WeatherSpec.Forecast gbForecast = new WeatherSpec.Forecast();
|
||||
gbForecast.minTemp = 10 + i + 273;
|
||||
gbForecast.maxTemp = 25 + i + 273;
|
||||
|
||||
gbForecast.conditionCode = 800; // clear
|
||||
weatherSpec.forecasts.add(gbForecast);
|
||||
}
|
||||
|
||||
Weather.getInstance().setWeatherSpec(weatherSpec);
|
||||
}
|
||||
|
||||
GBApplication.deviceService().onSendWeather(Weather.getInstance().getWeatherSpec());
|
||||
}
|
||||
});
|
||||
|
||||
Button setMusicInfoButton = findViewById(R.id.setMusicInfoButton);
|
||||
setMusicInfoButton.setOnClickListener(new View.OnClickListener() {
|
||||
|
@ -184,6 +184,8 @@ public class FindPhoneActivity extends AbstractGBActivity {
|
||||
stopVibration();
|
||||
stopSound();
|
||||
|
||||
GBApplication.deviceService().onPhoneFound();
|
||||
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
unregisterReceiver(mReceiver);
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@ -184,6 +185,18 @@ public class SettingsActivity extends AbstractSettingsActivity {
|
||||
|
||||
});
|
||||
|
||||
pref = findPreference("cache_weather");
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
boolean doEnable = Boolean.TRUE.equals(newVal);
|
||||
|
||||
Weather.getInstance().setCacheFile(getCacheDir(), doEnable);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// If we didn't manage to initialize file logging, disable the preference
|
||||
if (!GBApplication.getLogging().isFileLoggerInitialized()) {
|
||||
pref.setEnabled(false);
|
||||
|
@ -18,12 +18,18 @@ package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
|
||||
|
||||
public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_LANGUAGE = "language";
|
||||
public static final String PREF_LANGUAGE_AUTO = "auto";
|
||||
public static final String PREF_DATEFORMAT = "dateformat";
|
||||
public static final String PREF_TIMEFORMAT = "timeformat";
|
||||
public static final String PREF_TIMEFORMAT_24H = "24h";
|
||||
public static final String PREF_TIMEFORMAT_12H = "am/pm";
|
||||
public static final String PREF_TIMEFORMAT_AUTO = "auto";
|
||||
public static final String PREF_WEARLOCATION = "wearlocation";
|
||||
public static final String PREF_VIBRATION_ENABLE = "vibration_enable";
|
||||
public static final String PREF_NOTIFICATION_ENABLE = "notification_enable";
|
||||
public static final String PREF_SCREEN_BRIGHTNESS = "screen_brightness";
|
||||
public static final String PREF_SCREEN_ORIENTATION = "screen_orientation";
|
||||
public static final String PREF_SCREEN_TIMEOUT = "screen_timeout";
|
||||
public static final String PREF_RESERVER_ALARMS_CALENDAR = "reserve_alarms_calendar";
|
||||
public static final String PREF_RESERVER_REMINDERS_CALENDAR = "reserve_reminders_calendar";
|
||||
public static final String PREF_ALLOW_HIGH_MTU = "allow_high_mtu";
|
||||
@ -67,6 +73,14 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_DISPLAY_ON_LIFT_END = "display_on_lift_end";
|
||||
public static final String PREF_DISPLAY_ON_LIFT_SENSITIVITY = "display_on_lift_sensitivity";
|
||||
|
||||
public static final String PREF_ALWAYS_ON_DISPLAY_MODE = "always_on_display_mode";
|
||||
public static final String PREF_ALWAYS_ON_DISPLAY_START = "always_on_display_start";
|
||||
public static final String PREF_ALWAYS_ON_DISPLAY_END = "always_on_display_end";
|
||||
public static final String PREF_ALWAYS_ON_DISPLAY_OFF = "off";
|
||||
public static final String PREF_ALWAYS_ON_DISPLAY_AUTOMATIC = "automatic";
|
||||
public static final String PREF_ALWAYS_ON_DISPLAY_ALWAYS = "always";
|
||||
public static final String PREF_ALWAYS_ON_DISPLAY_SCHEDULED = "scheduled";
|
||||
|
||||
public static final String PREF_SLEEP_TIME = "prefs_enable_sleep_time";
|
||||
public static final String PREF_SLEEP_TIME_START = "prefs_sleep_time_start";
|
||||
public static final String PREF_SLEEP_TIME_END = "prefs_sleep_time_end";
|
||||
@ -95,8 +109,13 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_HEARTRATE_MEASUREMENT_INTERVAL = "heartrate_measurement_interval";
|
||||
public static final String PREF_HEARTRATE_ACTIVITY_MONITORING = "heartrate_activity_monitoring";
|
||||
public static final String PREF_HEARTRATE_ALERT_ENABLED = "heartrate_alert_enabled";
|
||||
public static final String PREF_HEARTRATE_ALERT_THRESHOLD = "heartrate_alert_threshold";
|
||||
public static final String PREF_HEARTRATE_ALERT_HIGH_THRESHOLD = "heartrate_alert_threshold";
|
||||
public static final String PREF_HEARTRATE_ALERT_LOW_THRESHOLD = "heartrate_alert_low_threshold";
|
||||
public static final String PREF_HEARTRATE_STRESS_MONITORING = "heartrate_stress_monitoring";
|
||||
public static final String PREF_HEARTRATE_STRESS_RELAXATION_REMINDER = "heartrate_stress_relaxation_reminder";
|
||||
public static final String PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING = "heartrate_sleep_breathing_quality_monitoring";
|
||||
public static final String PREF_SPO2_ALL_DAY_MONITORING = "spo2_all_day_monitoring_enabled";
|
||||
public static final String PREF_SPO2_LOW_ALERT_THRESHOLD = "spo2_low_alert_threshold";
|
||||
|
||||
public static final String PREF_AUTOHEARTRATE_SWITCH = "pref_autoheartrate_switch";
|
||||
public static final String PREF_AUTOHEARTRATE_SLEEP = "pref_autoheartrate_sleep";
|
||||
@ -116,6 +135,7 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_DO_NOT_DISTURB_LIFT_WRIST = "do_not_disturb_lift_wrist";
|
||||
public static final String PREF_DO_NOT_DISTURB_OFF = "off";
|
||||
public static final String PREF_DO_NOT_DISTURB_AUTOMATIC = "automatic";
|
||||
public static final String PREF_DO_NOT_DISTURB_ALWAYS = "always";
|
||||
public static final String PREF_DO_NOT_DISTURB_SCHEDULED = "scheduled";
|
||||
|
||||
public static final String PREF_WORKOUT_START_ON_PHONE = "workout_start_on_phone";
|
||||
@ -126,6 +146,7 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_AUTOLIGHT = "autolight";
|
||||
public static final String PREF_AUTOREMOVE_MESSAGE = "autoremove_message";
|
||||
public static final String PREF_AUTOREMOVE_NOTIFICATIONS = "autoremove_notifications";
|
||||
public static final String PREF_SCREEN_ON_ON_NOTIFICATIONS = "screen_on_on_notifications";
|
||||
public static final String PREF_OPERATING_SOUNDS = "operating_sounds";
|
||||
public static final String PREF_KEY_VIBRATION = "key_vibration";
|
||||
public static final String PREF_FAKE_RING_DURATION = "fake_ring_duration";
|
||||
|
@ -51,7 +51,6 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.CalBlacklistActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
@ -361,7 +360,22 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
});
|
||||
}
|
||||
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_ALERT_HIGH_THRESHOLD);
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_ALERT_LOW_THRESHOLD);
|
||||
|
||||
final ListPreference heartrateMeasurementInterval = findPreference(PREF_HEARTRATE_MEASUREMENT_INTERVAL);
|
||||
final ListPreference heartrateAlertHigh = findPreference(PREF_HEARTRATE_ALERT_HIGH_THRESHOLD);
|
||||
final ListPreference heartrateAlertLow = findPreference(PREF_HEARTRATE_ALERT_LOW_THRESHOLD);
|
||||
// Newer devices that have low alert threshold can only use it if measurement interval is smart (-1) or 1 minute
|
||||
final boolean hrAlertsNeedSmartOrOne = heartrateAlertHigh != null && heartrateAlertLow != null && heartrateMeasurementInterval != null;
|
||||
if (hrAlertsNeedSmartOrOne) {
|
||||
final boolean hrMonitoringIsSmartOrOne = heartrateMeasurementInterval.getValue().equals("60") ||
|
||||
heartrateMeasurementInterval.getValue().equals("-1");
|
||||
|
||||
heartrateAlertHigh.setEnabled(hrMonitoringIsSmartOrOne);
|
||||
heartrateAlertLow.setEnabled(hrMonitoringIsSmartOrOne);
|
||||
}
|
||||
|
||||
if (heartrateMeasurementInterval != null) {
|
||||
final SwitchPreference activityMonitoring = findPreference(PREF_HEARTRATE_ACTIVITY_MONITORING);
|
||||
final SwitchPreference heartrateAlertEnabled = findPreference(PREF_HEARTRATE_ALERT_ENABLED);
|
||||
@ -379,7 +393,15 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
if (heartrateAlertEnabled != null) {
|
||||
heartrateAlertEnabled.setEnabled(isMeasurementIntervalEnabled);
|
||||
}
|
||||
if (stressMonitoring != null) {
|
||||
if (hrAlertsNeedSmartOrOne) {
|
||||
// Same as above, check if smart or 1 minute
|
||||
final boolean hrMonitoringIsSmartOrOne = newVal.equals("60") || newVal.equals("-1");
|
||||
|
||||
heartrateAlertHigh.setEnabled(hrMonitoringIsSmartOrOne);
|
||||
heartrateAlertLow.setEnabled(hrMonitoringIsSmartOrOne);
|
||||
}
|
||||
if (stressMonitoring != null && !hrAlertsNeedSmartOrOne) {
|
||||
// Newer devices (that have hrAlertsNeedSmartOrOne) also don't need HR monitoring for stress monitoring
|
||||
stressMonitoring.setEnabled(isMeasurementIntervalEnabled);
|
||||
}
|
||||
|
||||
@ -395,7 +417,8 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
if (heartrateAlertEnabled != null) {
|
||||
heartrateAlertEnabled.setEnabled(isMeasurementIntervalEnabled);
|
||||
}
|
||||
if (stressMonitoring != null) {
|
||||
if (stressMonitoring != null && !hrAlertsNeedSmartOrOne) {
|
||||
// Newer devices (that have hrAlertsNeedSmartOrOne) also don't need HR monitoring for stress monitoring
|
||||
stressMonitoring.setEnabled(isMeasurementIntervalEnabled);
|
||||
}
|
||||
}
|
||||
@ -414,7 +437,9 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
addPreferenceHandlerFor(PREF_WEARLOCATION);
|
||||
addPreferenceHandlerFor(PREF_VIBRATION_ENABLE);
|
||||
addPreferenceHandlerFor(PREF_NOTIFICATION_ENABLE);
|
||||
addPreferenceHandlerFor(PREF_SCREEN_BRIGHTNESS);
|
||||
addPreferenceHandlerFor(PREF_SCREEN_ORIENTATION);
|
||||
addPreferenceHandlerFor(PREF_SCREEN_TIMEOUT);
|
||||
addPreferenceHandlerFor(PREF_TIMEFORMAT);
|
||||
addPreferenceHandlerFor(PREF_BUTTON_1_FUNCTION_SHORT);
|
||||
addPreferenceHandlerFor(PREF_BUTTON_2_FUNCTION_SHORT);
|
||||
@ -452,21 +477,22 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
addPreferenceHandlerFor(PREF_AUTOHEARTRATE_START);
|
||||
addPreferenceHandlerFor(PREF_AUTOHEARTRATE_END);
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_ACTIVITY_MONITORING);
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_ALERT_THRESHOLD);
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_ALERT_ENABLED);
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_STRESS_MONITORING);
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_STRESS_RELAXATION_REMINDER);
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING);
|
||||
addPreferenceHandlerFor(PREF_SPO2_ALL_DAY_MONITORING);
|
||||
addPreferenceHandlerFor(PREF_SPO2_LOW_ALERT_THRESHOLD);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_START);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_END);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_START);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_END);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_LIFT_WRIST);
|
||||
addPreferenceHandlerFor(PREF_FIND_PHONE);
|
||||
addPreferenceHandlerFor(PREF_FIND_PHONE_DURATION);
|
||||
addPreferenceHandlerFor(PREF_AUTOLIGHT);
|
||||
addPreferenceHandlerFor(PREF_AUTOREMOVE_MESSAGE);
|
||||
addPreferenceHandlerFor(PREF_AUTOREMOVE_NOTIFICATIONS);
|
||||
addPreferenceHandlerFor(PREF_SCREEN_ON_ON_NOTIFICATIONS);
|
||||
addPreferenceHandlerFor(PREF_KEY_VIBRATION);
|
||||
addPreferenceHandlerFor(PREF_OPERATING_SOUNDS);
|
||||
addPreferenceHandlerFor(PREF_FAKE_RING_DURATION);
|
||||
@ -627,6 +653,49 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
});
|
||||
}
|
||||
|
||||
final String alwaysOnDisplayState = prefs.getString(PREF_ALWAYS_ON_DISPLAY_MODE, PREF_ALWAYS_ON_DISPLAY_OFF);
|
||||
boolean alwaysOnDisplayScheduled = alwaysOnDisplayState.equals(PREF_ALWAYS_ON_DISPLAY_SCHEDULED);
|
||||
boolean alwaysOnDisplayOff = alwaysOnDisplayState.equals(PREF_ALWAYS_ON_DISPLAY_OFF);
|
||||
|
||||
final Preference alwaysOnDisplayStart = findPreference(PREF_ALWAYS_ON_DISPLAY_START);
|
||||
if (alwaysOnDisplayStart != null) {
|
||||
alwaysOnDisplayStart.setEnabled(alwaysOnDisplayScheduled);
|
||||
alwaysOnDisplayStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
notifyPreferenceChanged(PREF_ALWAYS_ON_DISPLAY_START);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Preference alwaysOnDisplayEnd = findPreference(PREF_ALWAYS_ON_DISPLAY_END);
|
||||
if (alwaysOnDisplayEnd != null) {
|
||||
alwaysOnDisplayEnd.setEnabled(alwaysOnDisplayScheduled);
|
||||
alwaysOnDisplayEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
notifyPreferenceChanged(PREF_ALWAYS_ON_DISPLAY_END);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Preference alwaysOnDisplayMode = findPreference(PREF_ALWAYS_ON_DISPLAY_MODE);
|
||||
if (alwaysOnDisplayMode != null) {
|
||||
alwaysOnDisplayMode.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
final boolean scheduled = PREF_ALWAYS_ON_DISPLAY_SCHEDULED.equals(newVal.toString());
|
||||
final boolean off = PREF_ALWAYS_ON_DISPLAY_OFF.equals(newVal.toString());
|
||||
alwaysOnDisplayStart.setEnabled(scheduled);
|
||||
alwaysOnDisplayEnd.setEnabled(scheduled);
|
||||
notifyPreferenceChanged(PREF_ALWAYS_ON_DISPLAY_MODE);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Preference displayOnLiftStart = findPreference(PREF_DISPLAY_ON_LIFT_START);
|
||||
if (displayOnLiftStart != null) {
|
||||
displayOnLiftStart.setEnabled(displayOnLiftScheduled);
|
||||
@ -729,6 +798,26 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
||||
});
|
||||
}
|
||||
|
||||
final Preference cannedMessagesGeneric = findPreference("canned_messages_generic_send");
|
||||
if (cannedMessagesGeneric != null) {
|
||||
cannedMessagesGeneric.setOnPreferenceClickListener(new androidx.preference.Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(androidx.preference.Preference preference) {
|
||||
final ArrayList<String> messages = new ArrayList<>();
|
||||
for (int i = 1; i <= 16; i++) {
|
||||
String message = prefs.getString("canned_reply_" + i, null);
|
||||
if (message != null && !message.equals("")) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
final CannedMessagesSpec cannedMessagesSpec = new CannedMessagesSpec();
|
||||
cannedMessagesSpec.type = CannedMessagesSpec.TYPE_GENERIC;
|
||||
cannedMessagesSpec.cannedMessages = messages.toArray(new String[0]);
|
||||
GBApplication.deviceService().onSetCannedMessages(cannedMessagesSpec);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setInputTypeFor(HuamiConst.PREF_BUTTON_ACTION_BROADCAST_DELAY, InputType.TYPE_CLASS_NUMBER);
|
||||
setInputTypeFor(HuamiConst.PREF_BUTTON_ACTION_PRESS_MAX_INTERVAL, InputType.TYPE_CLASS_NUMBER);
|
||||
setInputTypeFor(HuamiConst.PREF_BUTTON_ACTION_PRESS_COUNT, InputType.TYPE_CLASS_NUMBER);
|
||||
|
@ -636,7 +636,7 @@ public class DBHelper {
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
|
||||
int reservedSlots = prefs.getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR, 9);
|
||||
int reservedSlots = prefs.getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR, coordinator.supportsCalendarEvents() ? 0 : 9);
|
||||
|
||||
final int reminderSlots = coordinator.getReminderSlotCount();
|
||||
|
||||
|
@ -20,6 +20,13 @@ package nodomain.freeyourgadget.gadgetbridge.deviceevents;
|
||||
public class GBDeviceEventCallControl extends GBDeviceEvent {
|
||||
public Event event = Event.UNKNOWN;
|
||||
|
||||
public GBDeviceEventCallControl() {
|
||||
}
|
||||
|
||||
public GBDeviceEventCallControl(final Event event) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
public enum Event {
|
||||
UNKNOWN,
|
||||
ACCEPT,
|
||||
|
@ -68,7 +68,9 @@ public class GBDeviceEventUpdatePreferences extends GBDeviceEvent {
|
||||
|
||||
LOG.debug("Updating {} = {}", key, value);
|
||||
|
||||
if (value instanceof Integer) {
|
||||
if (value == null) {
|
||||
editor.remove(key);
|
||||
} else if (value instanceof Integer) {
|
||||
editor.putInt(key, (Integer) value);
|
||||
} else if (value instanceof Boolean) {
|
||||
editor.putBoolean(key, (Boolean) value);
|
||||
|
@ -20,8 +20,16 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class GBDeviceEventVersionInfo extends GBDeviceEvent {
|
||||
public String fwVersion = GBApplication.getContext().getString(R.string.n_a);
|
||||
public String hwVersion = GBApplication.getContext().getString(R.string.n_a);
|
||||
public String fwVersion = "N/A";
|
||||
public String hwVersion = "N/A";
|
||||
|
||||
public GBDeviceEventVersionInfo() {
|
||||
if (GBApplication.getContext() != null) {
|
||||
// Only get from context if there is one (eg. not in unit tests)
|
||||
this.fwVersion = GBApplication.getContext().getString(R.string.n_a);
|
||||
this.hwVersion = GBApplication.getContext().getString(R.string.n_a);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@ -264,6 +264,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(final GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUnicodeEmojis() {
|
||||
return false;
|
||||
|
@ -92,6 +92,8 @@ public interface EventHandler {
|
||||
|
||||
void onFindDevice(boolean start);
|
||||
|
||||
void onPhoneFound();
|
||||
|
||||
void onSetConstantVibration(int integer);
|
||||
|
||||
void onScreenshotReq();
|
||||
|
@ -0,0 +1,24 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami;
|
||||
|
||||
public enum AlwaysOnDisplay {
|
||||
OFF,
|
||||
AUTO,
|
||||
SCHEDULED,
|
||||
ALWAYS
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Huami2021Coordinator.class);
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(final GBDevice device) {
|
||||
// TODO: One-shot HR measures are not working, so let's disable this for now
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
// TODO: It's supported by the devices, but not yet implemented
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUnicodeEmojis() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
// TODO: It's supported by the devices, but not yet implemented
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
// TODO: It's supported by the devices, but not yet implemented
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
// TODO: It's supported by the devices, but not yet implemented
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMusicInfo() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWorldClocksSlotCount() {
|
||||
// TODO: It's supported, but not implemented - even in the official app
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO: It's supported by the devices, but not yet implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmSnoozing() {
|
||||
// All alarms snooze by default, there doesn't seem to be a flag that disables it
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReminderSlotCount() {
|
||||
return 50;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_REQUIRE_KEY;
|
||||
}
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami;
|
||||
|
||||
public class Huami2021Service {
|
||||
/**
|
||||
* Endpoints for 2021 chunked protocol
|
||||
*/
|
||||
public static final short CHUNKED2021_ENDPOINT_HTTP = 0x0001;
|
||||
public static final short CHUNKED2021_ENDPOINT_CALENDAR = 0x0007;
|
||||
public static final short CHUNKED2021_ENDPOINT_CONFIG = 0x000a;
|
||||
public static final short CHUNKED2021_ENDPOINT_ICONS = 0x000d;
|
||||
public static final short CHUNKED2021_ENDPOINT_WEATHER = 0x000e;
|
||||
public static final short CHUNKED2021_ENDPOINT_ALARMS = 0x000f;
|
||||
public static final short CHUNKED2021_ENDPOINT_CANNED_MESSAGES = 0x0013;
|
||||
public static final short CHUNKED2021_ENDPOINT_USER_INFO = 0x0017;
|
||||
public static final short CHUNKED2021_ENDPOINT_STEPS = 0x0016;
|
||||
public static final short CHUNKED2021_ENDPOINT_VIBRATION_PATTERNS = 0x0018;
|
||||
public static final short CHUNKED2021_ENDPOINT_WORKOUT = 0x0019;
|
||||
public static final short CHUNKED2021_ENDPOINT_FIND_DEVICE = 0x001a;
|
||||
public static final short CHUNKED2021_ENDPOINT_MUSIC = 0x001b;
|
||||
public static final short CHUNKED2021_ENDPOINT_HEARTRATE = 0x001d;
|
||||
public static final short CHUNKED2021_ENDPOINT_NOTIFICATIONS = 0x001e;
|
||||
public static final short CHUNKED2021_ENDPOINT_DISPLAY_ITEMS = 0x0026;
|
||||
public static final short CHUNKED2021_ENDPOINT_BATTERY = 0x0029;
|
||||
public static final short CHUNKED2021_ENDPOINT_REMINDERS = 0x0038;
|
||||
public static final short CHUNKED2021_ENDPOINT_SILENT_MODE = 0x003b;
|
||||
public static final short CHUNKED2021_ENDPOINT_AUTH = 0x0082;
|
||||
public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x0090;
|
||||
|
||||
/**
|
||||
* HTTP, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_HTTP}.
|
||||
*/
|
||||
public static final byte HTTP_CMD_REQUEST = 0x01;
|
||||
public static final byte HTTP_CMD_RESPONSE = 0x02;
|
||||
public static final byte HTTP_RESPONSE_SUCCESS = 0x01;
|
||||
public static final byte HTTP_RESPONSE_NO_INTERNET = 0x02;
|
||||
|
||||
/**
|
||||
* Alarms, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_ALARMS}.
|
||||
*/
|
||||
public static final byte ALARMS_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte ALARMS_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte ALARMS_CMD_CREATE = 0x03;
|
||||
public static final byte ALARMS_CMD_CREATE_ACK = 0x04;
|
||||
public static final byte ALARMS_CMD_DELETE = 0x05;
|
||||
public static final byte ALARMS_CMD_DELETE_ACK = 0x06;
|
||||
public static final byte ALARMS_CMD_UPDATE = 0x07;
|
||||
public static final byte ALARMS_CMD_UPDATE_ACK = 0x08;
|
||||
public static final byte ALARMS_CMD_REQUEST = 0x09;
|
||||
public static final byte ALARMS_CMD_RESPONSE = 0x0a;
|
||||
public static final byte ALARMS_CMD_NOTIFY_CHANGE = 0x0f;
|
||||
public static final int ALARM_IDX_FLAGS = 0;
|
||||
public static final int ALARM_IDX_POSITION = 1;
|
||||
public static final int ALARM_IDX_HOUR = 2;
|
||||
public static final int ALARM_IDX_MINUTE = 3;
|
||||
public static final int ALARM_IDX_REPETITION = 4;
|
||||
public static final int ALARM_FLAG_SMART = 0x01;
|
||||
public static final int ALARM_FLAG_UNKNOWN_2 = 0x02;
|
||||
public static final int ALARM_FLAG_ENABLED = 0x04;
|
||||
|
||||
/**
|
||||
* Display Items, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_DISPLAY_ITEMS}.
|
||||
*/
|
||||
public static final byte DISPLAY_ITEMS_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_CMD_REQUEST = 0x03;
|
||||
public static final byte DISPLAY_ITEMS_CMD_RESPONSE = 0x04;
|
||||
public static final byte DISPLAY_ITEMS_CMD_CREATE = 0x05;
|
||||
public static final byte DISPLAY_ITEMS_CMD_CREATE_ACK = 0x06;
|
||||
public static final byte DISPLAY_ITEMS_MENU = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_SHORTCUTS = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_MAIN = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_MORE = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_DISABLED = 0x03;
|
||||
|
||||
/**
|
||||
* Find Device, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_FIND_DEVICE}.
|
||||
*/
|
||||
public static final byte FIND_BAND_ONESHOT = 0x03;
|
||||
public static final byte FIND_BAND_ACK = 0x04;
|
||||
public static final byte FIND_PHONE_START = 0x11;
|
||||
public static final byte FIND_PHONE_ACK = 0x12;
|
||||
public static final byte FIND_PHONE_STOP_FROM_BAND = 0x13;
|
||||
public static final byte FIND_PHONE_STOP_FROM_PHONE = 0x14;
|
||||
|
||||
/**
|
||||
* Steps, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_STEPS}.
|
||||
*/
|
||||
public static final byte STEPS_CMD_GET = 0x03;
|
||||
public static final byte STEPS_CMD_REPLY = 0x04;
|
||||
public static final byte STEPS_CMD_ENABLE_REALTIME = 0x05;
|
||||
public static final byte STEPS_CMD_ENABLE_REALTIME_ACK = 0x06;
|
||||
public static final byte STEPS_CMD_REALTIME_NOTIFICATION = 0x07;
|
||||
|
||||
/**
|
||||
* Vibration Patterns, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_VIBRATION_PATTERNS}.
|
||||
*/
|
||||
public static final byte VIBRATION_PATTERN_SET = 0x03;
|
||||
public static final byte VIBRATION_PATTERN_ACK = 0x04;
|
||||
|
||||
/**
|
||||
* Battery, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_BATTERY}.
|
||||
*/
|
||||
public static final byte BATTERY_REQUEST = 0x03;
|
||||
public static final byte BATTERY_REPLY = 0x04;
|
||||
|
||||
/**
|
||||
* Silent Mode, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_SILENT_MODE}.
|
||||
*/
|
||||
public static final byte SILENT_MODE_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte SILENT_MODE_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
// Notify silent mode, from phone
|
||||
public static final byte SILENT_MODE_CMD_NOTIFY_BAND = 0x03;
|
||||
public static final byte SILENT_MODE_CMD_NOTIFY_BAND_ACK = 0x04;
|
||||
// Query silent mode on phone, from band
|
||||
public static final byte SILENT_MODE_CMD_QUERY = 0x05;
|
||||
public static final byte SILENT_MODE_CMD_REPLY = 0x06;
|
||||
// Set silent mode on phone, from band
|
||||
// After this, phone sends ACK + NOTIFY
|
||||
public static final byte SILENT_MODE_CMD_SET = 0x07;
|
||||
public static final byte SILENT_MODE_CMD_ACK = 0x08;
|
||||
|
||||
/**
|
||||
* Canned Messages, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_CANNED_MESSAGES}.
|
||||
*/
|
||||
public static final byte CANNED_MESSAGES_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte CANNED_MESSAGES_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte CANNED_MESSAGES_CMD_REQUEST = 0x03;
|
||||
public static final byte CANNED_MESSAGES_CMD_RESPONSE = 0x04;
|
||||
public static final byte CANNED_MESSAGES_CMD_SET = 0x05;
|
||||
public static final byte CANNED_MESSAGES_CMD_SET_ACK = 0x06;
|
||||
public static final byte CANNED_MESSAGES_CMD_DELETE = 0x07;
|
||||
public static final byte CANNED_MESSAGES_CMD_DELETE_ACK = 0x08;
|
||||
public static final byte CANNED_MESSAGES_CMD_REPLY_SMS = 0x0b;
|
||||
public static final byte CANNED_MESSAGES_CMD_REPLY_SMS_ACK = 0x0c;
|
||||
public static final byte CANNED_MESSAGES_CMD_REPLY_SMS_CHECK = 0x0d;
|
||||
public static final byte CANNED_MESSAGES_CMD_REPLY_SMS_ALLOW = 0x0e;
|
||||
|
||||
/**
|
||||
* Notifications, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_HEARTRATE}.
|
||||
*/
|
||||
public static final byte HEART_RATE_CMD_REALTIME_SET = 0x04;
|
||||
public static final byte HEART_RATE_CMD_REALTIME_ACK = 0x05;
|
||||
public static final byte HEART_RATE_CMD_SLEEP = 0x06;
|
||||
public static final byte HEART_RATE_FALL_ASLEEP = 0x01;
|
||||
public static final byte HEART_RATE_WAKE_UP = 0x00;
|
||||
public static final byte HEART_RATE_REALTIME_MODE_STOP = 0x00;
|
||||
public static final byte HEART_RATE_REALTIME_MODE_START = 0x01;
|
||||
public static final byte HEART_RATE_REALTIME_MODE_CONTINUE = 0x02;
|
||||
|
||||
/**
|
||||
* Notifications, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_NOTIFICATIONS}.
|
||||
*/
|
||||
public static final byte NOTIFICATION_CMD_SEND = 0x03;
|
||||
public static final byte NOTIFICATION_CMD_REPLY = 0x04;
|
||||
public static final byte NOTIFICATION_CMD_DISMISS = 0x05;
|
||||
public static final byte NOTIFICATION_CMD_REPLY_ACK = 0x06;
|
||||
public static final byte NOTIFICATION_CMD_ICON_REQUEST = 0x10;
|
||||
public static final byte NOTIFICATION_CMD_ICON_REQUEST_ACK = 0x11;
|
||||
public static final byte NOTIFICATION_TYPE_NORMAL = (byte) 0xfa;
|
||||
public static final byte NOTIFICATION_TYPE_CALL = 0x03;
|
||||
public static final byte NOTIFICATION_TYPE_SMS = (byte) 0x05;
|
||||
public static final byte NOTIFICATION_SUBCMD_SHOW = 0x00;
|
||||
public static final byte NOTIFICATION_SUBCMD_DISMISS_FROM_PHONE = 0x02;
|
||||
public static final byte NOTIFICATION_DISMISS_NOTIFICATION = 0x03;
|
||||
public static final byte NOTIFICATION_DISMISS_MUTE_CALL = 0x02;
|
||||
public static final byte NOTIFICATION_DISMISS_REJECT_CALL = 0x01;
|
||||
public static final byte NOTIFICATION_CALL_STATE_START = 0x00;
|
||||
public static final byte NOTIFICATION_CALL_STATE_END = 0x02;
|
||||
|
||||
/**
|
||||
* Workout, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_WORKOUT}.
|
||||
*/
|
||||
public static final byte WORKOUT_CMD_GPS_LOCATION = 0x04;
|
||||
public static final byte WORKOUT_CMD_APP_OPEN = 0x20;
|
||||
public static final byte WORKOUT_CMD_STATUS = 0x11;
|
||||
public static final int WORKOUT_GPS_FLAG_STATUS = 0x1;
|
||||
public static final int WORKOUT_GPS_FLAG_POSITION = 0x40000;
|
||||
public static final byte WORKOUT_STATUS_START = 0x01;
|
||||
public static final byte WORKOUT_STATUS_END = 0x04;
|
||||
|
||||
/**
|
||||
* Music, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_MUSIC}.
|
||||
*/
|
||||
public static final byte MUSIC_CMD_MEDIA_INFO = 0x03;
|
||||
public static final byte MUSIC_CMD_APP_STATE = 0x04;
|
||||
public static final byte MUSIC_CMD_BUTTON_PRESS = 0x05;
|
||||
public static final byte MUSIC_APP_OPEN = 0x01;
|
||||
public static final byte MUSIC_APP_CLOSE = 0x02;
|
||||
public static final byte MUSIC_BUTTON_PLAY = 0x00;
|
||||
public static final byte MUSIC_BUTTON_PAUSE = 0x01;
|
||||
public static final byte MUSIC_BUTTON_NEXT = 0x03;
|
||||
public static final byte MUSIC_BUTTON_PREVIOUS = 0x04;
|
||||
public static final byte MUSIC_BUTTON_VOLUME_UP = 0x05;
|
||||
public static final byte MUSIC_BUTTON_VOLUME_DOWN = 0x06;
|
||||
|
||||
/**
|
||||
* Config, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_CONFIG}.
|
||||
*/
|
||||
public static final byte CONFIG_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte CONFIG_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte CONFIG_CMD_REQUEST = 0x03;
|
||||
public static final byte CONFIG_CMD_RESPONSE = 0x04;
|
||||
public static final byte CONFIG_CMD_SET = 0x05;
|
||||
public static final byte CONFIG_CMD_ACK = 0x06;
|
||||
public static final byte CONFIG_REQUEST_TYPE_SPECIFIC = 0x00;
|
||||
public static final byte CONFIG_REQUEST_TYPE_ALL = 0x01; // Don't know how to parse them properly
|
||||
|
||||
/**
|
||||
* Config, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_ICONS}.
|
||||
*/
|
||||
public static final byte ICONS_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte ICONS_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte ICONS_CMD_SEND_REQUEST = 0x03;
|
||||
public static final byte ICONS_CMD_SEND_RESPONSE = 0x04;
|
||||
public static final byte ICONS_CMD_DATA_SEND = 0x10;
|
||||
public static final byte ICONS_CMD_DATA_ACK = 0x11;
|
||||
|
||||
/**
|
||||
* Reminders, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_REMINDERS}.
|
||||
*/
|
||||
public static final byte REMINDERS_CMD_REQUEST = 0x03;
|
||||
public static final byte REMINDERS_CMD_RESPONSE = 0x04;
|
||||
public static final byte REMINDERS_CMD_CREATE = 0x05;
|
||||
public static final byte REMINDERS_CMD_CREATE_ACK = 0x06;
|
||||
public static final byte REMINDERS_CMD_UPDATE = 0x07;
|
||||
public static final byte REMINDERS_CMD_UPDATE_ACK = 0x08;
|
||||
public static final byte REMINDERS_CMD_DELETE = 0x09;
|
||||
public static final byte REMINDERS_CMD_DELETE_ACK = 0x0a;
|
||||
public static final int REMINDER_FLAG_ENABLED = 0x0001;
|
||||
public static final int REMINDER_FLAG_TEXT = 0x0008;
|
||||
public static final int REMINDER_FLAG_REPEAT_MONTH = 0x1000;
|
||||
public static final int REMINDER_FLAG_REPEAT_YEAR = 0x2000;
|
||||
|
||||
/**
|
||||
* Calendar, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_CALENDAR}.
|
||||
*/
|
||||
public static final byte CALENDAR_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte CALENDAR_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte CALENDAR_CMD_EVENTS_REQUEST = 0x05;
|
||||
public static final byte CALENDAR_CMD_EVENTS_RESPONSE = 0x06;
|
||||
public static final byte CALENDAR_CMD_CREATE_EVENT = 0x07;
|
||||
public static final byte CALENDAR_CMD_CREATE_EVENT_ACK = 0x08;
|
||||
public static final byte CALENDAR_CMD_DELETE_EVENT = 0x09;
|
||||
public static final byte CALENDAR_CMD_DELETE_EVENT_ACK = 0x0a;
|
||||
}
|
@ -56,6 +56,7 @@ public class HuamiConst {
|
||||
public static final String AMAZFIT_NEO_NAME = "Amazfit Neo";
|
||||
public static final String AMAZFIT_X = "Amazfit X";
|
||||
|
||||
public static final String XIAOMI_SMART_BAND7_NAME = "Xiaomi Smart Band 7";
|
||||
|
||||
public static final String PREF_DISPLAY_ITEMS = "display_items";
|
||||
public static final String PREF_DISPLAY_ITEMS_SORTABLE = "display_items_sortable";
|
||||
|
@ -97,7 +97,9 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFlashing() { return true; }
|
||||
public boolean supportsFlashing() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
@ -148,22 +150,26 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return DateTimeDisplay.DATE_TIME;
|
||||
}
|
||||
|
||||
public static ActivateDisplayOnLift getActivateDisplayOnLiftWrist(Context context, String deviceAddress) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
||||
|
||||
String liftOff = context.getString(R.string.p_off);
|
||||
String liftOn = context.getString(R.string.p_on);
|
||||
String liftScheduled = context.getString(R.string.p_scheduled);
|
||||
|
||||
String pref = prefs.getString(DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT, liftOff);
|
||||
|
||||
if (liftOn.equals(pref)) {
|
||||
return ActivateDisplayOnLift.ON;
|
||||
} else if (liftScheduled.equals(pref)) {
|
||||
return ActivateDisplayOnLift.SCHEDULED;
|
||||
public static AlwaysOnDisplay getAlwaysOnDisplay(final String deviceAddress) {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
||||
final String pref = prefs.getString(DeviceSettingsPreferenceConst.PREF_ALWAYS_ON_DISPLAY_MODE, DeviceSettingsPreferenceConst.PREF_ALWAYS_ON_DISPLAY_OFF);
|
||||
return AlwaysOnDisplay.valueOf(pref.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
return ActivateDisplayOnLift.OFF;
|
||||
public static Date getAlwaysOnDisplayStart(final String deviceAddress) {
|
||||
return getTimePreference(DeviceSettingsPreferenceConst.PREF_ALWAYS_ON_DISPLAY_START, "00:00", deviceAddress);
|
||||
}
|
||||
|
||||
public static Date getAlwaysOnDisplayEnd(final String deviceAddress) {
|
||||
return getTimePreference(DeviceSettingsPreferenceConst.PREF_ALWAYS_ON_DISPLAY_END, "00:00", deviceAddress);
|
||||
}
|
||||
|
||||
public static ActivateDisplayOnLift getActivateDisplayOnLiftWrist(Context context, String deviceAddress) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
||||
String liftOff = context.getString(R.string.p_off);
|
||||
String pref = prefs.getString(DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT, liftOff);
|
||||
|
||||
return ActivateDisplayOnLift.valueOf(pref.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static Date getDisplayOnLiftStart(String deviceAddress) {
|
||||
@ -184,20 +190,10 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
public static DisconnectNotificationSetting getDisconnectNotificationSetting(Context context, String deviceAddress) {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
|
||||
String liftOff = context.getString(R.string.p_off);
|
||||
String liftOn = context.getString(R.string.p_on);
|
||||
String liftScheduled = context.getString(R.string.p_scheduled);
|
||||
|
||||
String pref = prefs.getString(DeviceSettingsPreferenceConst.PREF_DISCONNECT_NOTIFICATION, liftOff);
|
||||
|
||||
if (liftOn.equals(pref)) {
|
||||
return DisconnectNotificationSetting.ON;
|
||||
} else if (liftScheduled.equals(pref)) {
|
||||
return DisconnectNotificationSetting.SCHEDULED;
|
||||
}
|
||||
|
||||
return DisconnectNotificationSetting.OFF;
|
||||
return DisconnectNotificationSetting.valueOf(pref.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static Date getDisconnectNotificationStart(String deviceAddress) {
|
||||
@ -267,6 +263,21 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return prefs.getBoolean(MiBandConst.PREF_SWIPE_UNLOCK, false);
|
||||
}
|
||||
|
||||
public static boolean getScreenOnOnNotification(String deviceAddress) {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS, false);
|
||||
}
|
||||
|
||||
public static int getScreenBrightness(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getInt(DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS, 50);
|
||||
}
|
||||
|
||||
public static int getScreenTimeout(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getInt(DeviceSettingsPreferenceConst.PREF_SCREEN_TIMEOUT, 5);
|
||||
}
|
||||
|
||||
public static boolean getExposeHRThirdParty(String deviceAddress) {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getBoolean(HuamiConst.PREF_EXPOSE_HR_THIRDPARTY, false);
|
||||
@ -297,9 +308,29 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HEARTRATE_ALERT_ENABLED, false);
|
||||
}
|
||||
|
||||
public static int getHeartrateAlertThreshold(String deviceAddress) throws IllegalArgumentException {
|
||||
public static int getHeartrateAlertHighThreshold(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getInt(DeviceSettingsPreferenceConst.PREF_HEARTRATE_ALERT_THRESHOLD, 150);
|
||||
return prefs.getInt(DeviceSettingsPreferenceConst.PREF_HEARTRATE_ALERT_HIGH_THRESHOLD, 150);
|
||||
}
|
||||
|
||||
public static int getHeartrateAlertLowThreshold(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getInt(DeviceSettingsPreferenceConst.PREF_HEARTRATE_ALERT_LOW_THRESHOLD, 45);
|
||||
}
|
||||
|
||||
public static boolean getHeartrateSleepBreathingQualityMonitoring(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING, false);
|
||||
}
|
||||
|
||||
public static boolean getSPO2AllDayMonitoring(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SPO2_ALL_DAY_MONITORING, false);
|
||||
}
|
||||
|
||||
public static int getSPO2AlertThreshold(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getInt(DeviceSettingsPreferenceConst.PREF_SPO2_LOW_ALERT_THRESHOLD, 0);
|
||||
}
|
||||
|
||||
public static boolean getHeartrateStressMonitoring(String deviceAddress) throws IllegalArgumentException {
|
||||
@ -307,6 +338,11 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HEARTRATE_STRESS_MONITORING, false);
|
||||
}
|
||||
|
||||
public static boolean getHeartrateStressRelaxationReminder(String deviceAddress) throws IllegalArgumentException {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HEARTRATE_STRESS_RELAXATION_REMINDER, false);
|
||||
}
|
||||
|
||||
public static boolean getBtConnectedAdvertising(String deviceAddress) {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_BT_CONNECTED_ADVERTISEMENT, false);
|
||||
@ -362,13 +398,7 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
String pref = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_OFF);
|
||||
|
||||
if (DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_AUTOMATIC.equals(pref)) {
|
||||
return DoNotDisturb.AUTOMATIC;
|
||||
} else if (DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SCHEDULED.equals(pref)) {
|
||||
return DoNotDisturb.SCHEDULED;
|
||||
}
|
||||
|
||||
return DoNotDisturb.OFF;
|
||||
return DoNotDisturb.valueOf(pref.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static boolean getDoNotDisturbLiftWrist(String deviceAddress) {
|
||||
|
@ -27,11 +27,12 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
|
||||
|
||||
public abstract class HuamiFWHelper extends AbstractMiBandFWHelper {
|
||||
protected HuamiFirmwareInfo firmwareInfo;
|
||||
protected AbstractHuamiFirmwareInfo firmwareInfo;
|
||||
|
||||
public HuamiFWHelper(Uri uri, Context context) throws IOException {
|
||||
super(uri, context);
|
||||
@ -68,6 +69,7 @@ public abstract class HuamiFWHelper extends AbstractMiBandFWHelper {
|
||||
resId = R.string.kind_resources;
|
||||
break;
|
||||
case FIRMWARE:
|
||||
case FIRMWARE_UIHH_2021_ZIP_WITH_CHANGELOG:
|
||||
resId = R.string.kind_firmware;
|
||||
break;
|
||||
case WATCHFACE:
|
||||
@ -118,8 +120,8 @@ public abstract class HuamiFWHelper extends AbstractMiBandFWHelper {
|
||||
public HuamiFirmwareType getFirmwareType() {
|
||||
return firmwareInfo.getFirmwareType();
|
||||
}
|
||||
public HuamiFirmwareInfo getFirmwareInfo() {
|
||||
|
||||
public AbstractHuamiFirmwareInfo getFirmwareInfo() {
|
||||
return firmwareInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -206,6 +206,14 @@ public class HuamiService {
|
||||
public static byte DND_BYTE_END_HOURS = 4;
|
||||
public static byte DND_BYTE_END_MINUTES = 5;
|
||||
|
||||
public static final byte MUSIC_FLAG_STATE = 0x01;
|
||||
public static final byte MUSIC_FLAG_ARTIST = 0x02;
|
||||
public static final byte MUSIC_FLAG_ALBUM = 0x04;
|
||||
public static final byte MUSIC_FLAG_TRACK = 0x08;
|
||||
public static final byte MUSIC_FLAG_DURATION = 0x10;
|
||||
public static final byte MUSIC_FLAG_NOTHING_PLAYING = 0x20;
|
||||
public static final byte MUSIC_FLAG_VOLUME = 0x40;
|
||||
|
||||
public static final byte RESPONSE = 0x10;
|
||||
|
||||
public static final byte SUCCESS = 0x01;
|
||||
@ -238,13 +246,6 @@ public class HuamiService {
|
||||
|
||||
public static final byte[] COMMAND_TEXT_NOTIFICATION = new byte[] {0x05, 0x01};
|
||||
|
||||
/**
|
||||
* Endpoints for 2021 chunked protocol
|
||||
*/
|
||||
public static final short CHUNKED2021_ENDPOINT_AUTH = 0x0082;
|
||||
public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x0090;
|
||||
public static final short CHUNKED2021_ENDPOINT_SMSREPLY = 0x0013;
|
||||
|
||||
public static final byte[] COMMAND_ENABLE_HOURLY_CHIME = new byte[] { (byte) 0xfe, 0x0b, 0x00, 0x01, 0x0a, 0x00, 0x16, 0x00 };
|
||||
public static final byte[] COMMAND_DISABLE_HOURLY_CHIME = new byte[] { (byte) 0xfe, 0x0b, 0x00, 0x00 };
|
||||
|
||||
|
@ -0,0 +1,142 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami.miband7;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class MiBand7Coordinator extends Huami2021Coordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBand7Coordinator.class);
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(final GBDeviceCandidate candidate) {
|
||||
try {
|
||||
final BluetoothDevice device = candidate.getDevice();
|
||||
final String name = device.getName();
|
||||
if (name != null && name.startsWith(HuamiConst.XIAOMI_SMART_BAND7_NAME)) {
|
||||
return DeviceType.MIBAND7;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("unable to check device support", e);
|
||||
}
|
||||
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.MIBAND7;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
|
||||
final MiBand7FWInstallHandler handler = new MiBand7FWInstallHandler(uri, context);
|
||||
return handler.isValid() ? handler : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_header_time,
|
||||
//R.xml.devicesettings_timeformat,
|
||||
R.xml.devicesettings_dateformat_2,
|
||||
// TODO R.xml.devicesettings_world_clocks,
|
||||
|
||||
R.xml.devicesettings_header_display,
|
||||
R.xml.devicesettings_miband7_displayitems,
|
||||
R.xml.devicesettings_miband7_shortcuts,
|
||||
R.xml.devicesettings_nightmode,
|
||||
R.xml.devicesettings_liftwrist_display_sensitivity,
|
||||
R.xml.devicesettings_password,
|
||||
R.xml.devicesettings_always_on_display,
|
||||
R.xml.devicesettings_screen_timeout_5_to_15,
|
||||
R.xml.devicesettings_screen_brightness,
|
||||
|
||||
R.xml.devicesettings_header_health,
|
||||
R.xml.devicesettings_heartrate_sleep_alert_activity_stress_spo2,
|
||||
R.xml.devicesettings_inactivity_dnd_no_threshold,
|
||||
R.xml.devicesettings_goal_notification,
|
||||
|
||||
R.xml.devicesettings_header_workout,
|
||||
R.xml.devicesettings_workout_start_on_phone,
|
||||
R.xml.devicesettings_workout_send_gps_to_band,
|
||||
|
||||
R.xml.devicesettings_header_notifications,
|
||||
R.xml.devicesettings_vibrationpatterns,
|
||||
R.xml.devicesettings_donotdisturb_withauto_and_always,
|
||||
R.xml.devicesettings_screen_on_on_notifications,
|
||||
R.xml.devicesettings_autoremove_notifications,
|
||||
R.xml.devicesettings_canned_reply_16,
|
||||
R.xml.devicesettings_transliteration,
|
||||
|
||||
R.xml.devicesettings_header_calendar,
|
||||
R.xml.devicesettings_sync_calendar,
|
||||
|
||||
R.xml.devicesettings_header_other,
|
||||
R.xml.devicesettings_device_actions_without_not_wear,
|
||||
|
||||
R.xml.devicesettings_header_connection,
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_bt_connected_advertisement,
|
||||
R.xml.devicesettings_high_mtu,
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedLanguageSettings(GBDevice device) {
|
||||
return new String[]{
|
||||
"auto",
|
||||
"de_DE",
|
||||
"en_US",
|
||||
"es_ES",
|
||||
"fr_FR",
|
||||
"it_IT",
|
||||
"nl_NL",
|
||||
"pt_PT",
|
||||
"tr_TR",
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public PasswordCapabilityImpl.Mode getPasswordCapability() {
|
||||
return PasswordCapabilityImpl.Mode.NUMBERS_6;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami.miband7;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband7.MiBand7FirmwareInfo;
|
||||
|
||||
public class MiBand7FWHelper extends HuamiFWHelper {
|
||||
public MiBand7FWHelper(final Uri uri, final Context context) throws IOException {
|
||||
super(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineFirmwareInfo(final byte[] wholeFirmwareBytes) {
|
||||
firmwareInfo = new MiBand7FirmwareInfo(wholeFirmwareBytes);
|
||||
if (!firmwareInfo.isHeaderValid()) {
|
||||
throw new IllegalArgumentException("Not a Xiaomi Smart Band 7 firmware");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami.miband7;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
class MiBand7FWInstallHandler extends AbstractMiBandFWInstallHandler {
|
||||
MiBand7FWInstallHandler(Uri uri, Context context) {
|
||||
super(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFwUpgradeNotice() {
|
||||
return mContext.getString(R.string.fw_upgrade_notice_miband7, helper.getHumanFirmwareVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
|
||||
return new MiBand7FWHelper(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSupportedDeviceType(GBDevice device) {
|
||||
return device.getType() == DeviceType.MIBAND7;
|
||||
}
|
||||
}
|
@ -54,7 +54,7 @@ public abstract class AbstractMiBandFWHelper {
|
||||
}
|
||||
|
||||
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
|
||||
this.fw = FileUtils.readAll(in, 1024 * 1024 * 8); // 8.0 MB
|
||||
this.fw = FileUtils.readAll(in, 1024 * 1024 * 16); // 16.0 MB
|
||||
determineFirmwareInfo(fw);
|
||||
} catch (IOException ex) {
|
||||
throw ex; // pass through
|
||||
|
@ -19,5 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
public enum DoNotDisturb {
|
||||
OFF,
|
||||
AUTOMATIC,
|
||||
SCHEDULED
|
||||
SCHEDULED,
|
||||
ALWAYS,
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ public class NotificationListener extends NotificationListenerService {
|
||||
PendingIntent actionIntent = wearableAction.getActionIntent();
|
||||
Intent localIntent = new Intent();
|
||||
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
if(wearableAction.getRemoteInputs()!=null) {
|
||||
if (wearableAction.getRemoteInputs() != null && wearableAction.getRemoteInputs().length > 0) {
|
||||
RemoteInput[] remoteInputs = wearableAction.getRemoteInputs();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putCharSequence(remoteInputs[0].getResultKey(), reply);
|
||||
@ -409,7 +409,7 @@ public class NotificationListener extends NotificationListenerService {
|
||||
if (act != null) {
|
||||
NotificationSpec.Action wearableAction = new NotificationSpec.Action();
|
||||
wearableAction.title = act.getTitle().toString();
|
||||
if(act.getRemoteInputs()!=null) {
|
||||
if (act.getRemoteInputs() != null && act.getRemoteInputs().length > 0) {
|
||||
wearableAction.type = NotificationSpec.Action.TYPE_WEARABLE_REPLY;
|
||||
} else {
|
||||
wearableAction.type = NotificationSpec.Action.TYPE_WEARABLE_SIMPLE;
|
||||
@ -780,8 +780,12 @@ public class NotificationListener extends NotificationListenerService {
|
||||
}
|
||||
|
||||
private void logNotification(StatusBarNotification sbn, boolean posted) {
|
||||
String infoMsg = (posted ? "Notification posted" : "Notification removed")
|
||||
+ ": " + sbn.getPackageName();
|
||||
String infoMsg = String.format(
|
||||
"Notification %d %s: %s",
|
||||
sbn.getId(),
|
||||
posted ? "posted" : "removed",
|
||||
sbn.getPackageName()
|
||||
);
|
||||
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
infoMsg += ": " + sbn.getNotification().category;
|
||||
|
@ -26,6 +26,7 @@ import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -346,6 +347,12 @@ public class GBDeviceService implements DeviceService {
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneFound() {
|
||||
Intent intent = createIntent().setAction(ACTION_PHONE_FOUND);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int intensity) {
|
||||
Intent intent = createIntent().setAction(ACTION_SET_CONSTANT_VIBRATION)
|
||||
@ -431,7 +438,7 @@ public class GBDeviceService implements DeviceService {
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
Intent intent = createIntent().setAction(ACTION_SEND_WEATHER)
|
||||
.putExtra(EXTRA_WEATHER, weatherSpec);
|
||||
.putExtra(EXTRA_WEATHER, (Parcelable) weatherSpec);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@ public interface DeviceService extends EventHandler {
|
||||
String ACTION_FETCH_RECORDED_DATA = PREFIX + ".action.fetch_activity_data";
|
||||
String ACTION_DISCONNECT = PREFIX + ".action.disconnect";
|
||||
String ACTION_FIND_DEVICE = PREFIX + ".action.find_device";
|
||||
String ACTION_PHONE_FOUND = PREFIX + ".action.phone_found";
|
||||
String ACTION_SET_CONSTANT_VIBRATION = PREFIX + ".action.set_constant_vibration";
|
||||
String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms";
|
||||
String ACTION_SAVE_ALARMS = PREFIX + ".action.save_alarms";
|
||||
|
@ -65,6 +65,7 @@ public enum DeviceType {
|
||||
AMAZFITTREXPRO(38, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_amazfit_trex_pro),
|
||||
AMAZFITPOP(39, R.drawable.ic_device_amazfit_bip, R.drawable.ic_device_amazfit_bip_disabled, R.string.devicetype_amazfit_pop),
|
||||
AMAZFITPOPPRO(10040, R.drawable.ic_device_amazfit_bip, R.drawable.ic_device_amazfit_bip_disabled, R.string.devicetype_amazfit_pop_pro),
|
||||
MIBAND7(10041, R.drawable.ic_device_miband6, R.drawable.ic_device_miband6_disabled, R.string.devicetype_miband7),
|
||||
HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_hplus),
|
||||
MAKIBESF68(41, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_makibes_f68),
|
||||
EXRIZUK8(42, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_exrizu_k8),
|
||||
|
@ -18,11 +18,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
public class RecordedDataTypes {
|
||||
public static int TYPE_ACTIVITY = 0x00000001;
|
||||
public static int TYPE_WORKOUTS = 0x00000002;
|
||||
public static int TYPE_GPS_TRACKS = 0x00000004;
|
||||
public static int TYPE_TEMPERATURE = 0x00000008;
|
||||
public static int TYPE_DEBUGLOGS = 0x00000010;
|
||||
public static final int TYPE_ACTIVITY = 0x00000001;
|
||||
public static final int TYPE_WORKOUTS = 0x00000002;
|
||||
public static final int TYPE_GPS_TRACKS = 0x00000004;
|
||||
public static final int TYPE_TEMPERATURE = 0x00000008;
|
||||
public static final int TYPE_DEBUGLOGS = 0x00000010;
|
||||
|
||||
public static int TYPE_ALL = (int)0xffffffff;
|
||||
public static final int TYPE_ALL = (int)0xffffffff;
|
||||
}
|
||||
|
@ -23,6 +23,12 @@ import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
public class Weather {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Weather.class);
|
||||
|
||||
@ -30,12 +36,15 @@ public class Weather {
|
||||
|
||||
private JSONObject reconstructedOWMForecast = null;
|
||||
|
||||
private File cacheFile;
|
||||
|
||||
public WeatherSpec getWeatherSpec() {
|
||||
return weatherSpec;
|
||||
}
|
||||
|
||||
public void setWeatherSpec(WeatherSpec weatherSpec) {
|
||||
this.weatherSpec = weatherSpec;
|
||||
saveToCache();
|
||||
}
|
||||
|
||||
public JSONObject createReconstructedOWMWeatherReply() {
|
||||
@ -971,4 +980,70 @@ public class Weather {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the weather cache file. If enabled and the current weather is null, load the cache file.
|
||||
*
|
||||
* @param cacheDir the cache directory, where the cache file will be created
|
||||
* @param enabled whether caching is enabled
|
||||
*/
|
||||
public void setCacheFile(final File cacheDir, final boolean enabled) {
|
||||
cacheFile = new File(cacheDir, "weatherCache.bin");
|
||||
|
||||
if (enabled) {
|
||||
LOG.info("Setting weather cache file to {}", cacheFile.getPath());
|
||||
|
||||
if (cacheFile.isFile() && weatherSpec == null) {
|
||||
try {
|
||||
final FileInputStream f = new FileInputStream(cacheFile);
|
||||
final ObjectInputStream o = new ObjectInputStream(f);
|
||||
|
||||
weatherSpec = (WeatherSpec) o.readObject();
|
||||
|
||||
o.close();
|
||||
f.close();
|
||||
} catch (final Throwable e) {
|
||||
LOG.error("Failed to read weather from cache", e);
|
||||
weatherSpec = null;
|
||||
cacheFile = null;
|
||||
}
|
||||
} else if (weatherSpec != null) {
|
||||
saveToCache();
|
||||
}
|
||||
} else {
|
||||
if (cacheFile.isFile()) {
|
||||
LOG.info("Deleting weather cache file {}", cacheFile.getPath());
|
||||
|
||||
try {
|
||||
cacheFile.delete();
|
||||
} catch (final Throwable e) {
|
||||
LOG.error("Failed to delete cache file", e);
|
||||
cacheFile = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current weather to cache, if a cache file is enabled and the weather is not null.
|
||||
*/
|
||||
public void saveToCache() {
|
||||
if (weatherSpec == null || cacheFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Loading weather from cache {}", cacheFile.getPath());
|
||||
|
||||
try {
|
||||
final FileOutputStream f = new FileOutputStream(cacheFile);
|
||||
final ObjectOutputStream o = new ObjectOutputStream(f);
|
||||
|
||||
o.writeObject(weatherSpec);
|
||||
|
||||
o.close();
|
||||
f.close();
|
||||
} catch (final Throwable e) {
|
||||
LOG.error("Failed to save weather to cache", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,11 +21,12 @@ package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
// FIXME: document me and my fields, including units
|
||||
public class WeatherSpec implements Parcelable {
|
||||
public class WeatherSpec implements Parcelable, Serializable {
|
||||
|
||||
public static final Creator<WeatherSpec> CREATOR = new Creator<WeatherSpec>() {
|
||||
@Override
|
||||
public WeatherSpec createFromParcel(Parcel in) {
|
||||
@ -38,6 +39,7 @@ public class WeatherSpec implements Parcelable {
|
||||
}
|
||||
};
|
||||
public static final int VERSION = 2;
|
||||
private static final long serialVersionUID = VERSION;
|
||||
public int timestamp;
|
||||
public String location;
|
||||
public int currentTemp;
|
||||
@ -106,7 +108,9 @@ public class WeatherSpec implements Parcelable {
|
||||
dest.writeList(forecasts);
|
||||
}
|
||||
|
||||
public static class Forecast implements Parcelable {
|
||||
public static class Forecast implements Parcelable, Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final Creator<Forecast> CREATOR = new Creator<Forecast>() {
|
||||
@Override
|
||||
public Forecast createFromParcel(Parcel in) {
|
||||
|
@ -108,6 +108,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FI
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_HEARTRATE_TEST;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_INSTALL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_PHONE_FOUND;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_POWER_OFF;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_READ_CONFIGURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO;
|
||||
@ -693,6 +694,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
deviceSupport.onFindDevice(start);
|
||||
break;
|
||||
}
|
||||
case ACTION_PHONE_FOUND: {
|
||||
deviceSupport.onPhoneFound();
|
||||
break;
|
||||
}
|
||||
case ACTION_SET_CONSTANT_VIBRATION: {
|
||||
int intensity = intent.getIntExtra(EXTRA_VIBRATION_INTENSITY, 0);
|
||||
deviceSupport.onSetConstantVibration(intensity);
|
||||
|
@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband3.MiBand
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband4.MiBand4Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband5.MiBand5Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband6.MiBand6Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband7.MiBand7Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppe.ZeppESupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.itag.ITagSupport;
|
||||
@ -179,6 +180,8 @@ public class DeviceSupportFactory {
|
||||
return new ServiceDeviceSupport(new MiBand5Support());
|
||||
case MIBAND6:
|
||||
return new ServiceDeviceSupport(new MiBand6Support());
|
||||
case MIBAND7:
|
||||
return new ServiceDeviceSupport(new MiBand7Support());
|
||||
case AMAZFITBIP:
|
||||
return new ServiceDeviceSupport(new AmazfitBipSupport());
|
||||
case AMAZFITBIP_LITE:
|
||||
|
@ -302,6 +302,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
delegate.onFindDevice(start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneFound() {
|
||||
if (checkBusy("phone found")) {
|
||||
return;
|
||||
}
|
||||
delegate.onPhoneFound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int intensity) {
|
||||
if (checkBusy("set constant vibration")) {
|
||||
|
@ -355,6 +355,11 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneFound() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
|
||||
|
@ -157,6 +157,10 @@ public class BLETypeConversions {
|
||||
return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8) | ((bytes[2] & 0xff) << 16) | ((bytes[3] & 0xff) << 24);
|
||||
}
|
||||
|
||||
public static int toUint32(byte[] bytes, int offset) {
|
||||
return (bytes[offset + 0] & 0xff) | ((bytes[offset + 1] & 0xff) << 8) | ((bytes[offset + 2] & 0xff) << 16) | ((bytes[offset + 3] & 0xff) << 24);
|
||||
}
|
||||
|
||||
public static byte[] fromUint16(int value) {
|
||||
return new byte[] {
|
||||
(byte) (value & 0xff),
|
||||
|
@ -0,0 +1,110 @@
|
||||
/* Copyright (C) 2017-2022 Andreas Shimokawa, José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
|
||||
public abstract class AbstractHuamiFirmwareInfo {
|
||||
private final byte[] bytes;
|
||||
|
||||
private final int crc16;
|
||||
private final int crc32;
|
||||
|
||||
protected final HuamiFirmwareType firmwareType;
|
||||
|
||||
public AbstractHuamiFirmwareInfo(byte[] bytes) {
|
||||
this.bytes = bytes;
|
||||
this.crc16 = CheckSums.getCRC16(bytes);
|
||||
this.crc32 = CheckSums.getCRC32(bytes);
|
||||
this.firmwareType = determineFirmwareType(bytes);
|
||||
}
|
||||
|
||||
public boolean isHeaderValid() {
|
||||
return getFirmwareType() != HuamiFirmwareType.INVALID;
|
||||
}
|
||||
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
}
|
||||
|
||||
public int[] getWhitelistedVersions() {
|
||||
return ArrayUtils.toIntArray(getCrcMap().keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of the firmware in number of bytes.
|
||||
*/
|
||||
public int getSize() {
|
||||
return bytes.length;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public int getCrc16() {
|
||||
return crc16;
|
||||
}
|
||||
|
||||
public int getCrc32() {
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public int getFirmwareVersion() {
|
||||
return getCrc16(); // HACK until we know how to determine the version from the fw bytes
|
||||
}
|
||||
|
||||
public HuamiFirmwareType getFirmwareType() {
|
||||
return firmwareType;
|
||||
}
|
||||
|
||||
public abstract String toVersion(int crc16);
|
||||
|
||||
public abstract boolean isGenerallyCompatibleWith(GBDevice device);
|
||||
|
||||
protected abstract Map<Integer, String> getCrcMap();
|
||||
|
||||
protected abstract HuamiFirmwareType determineFirmwareType(byte[] bytes);
|
||||
|
||||
public static boolean searchString32BitAligned(byte[] fwbytes, String findString) {
|
||||
ByteBuffer stringBuf = ByteBuffer.wrap((findString + "\0").getBytes());
|
||||
stringBuf.order(ByteOrder.BIG_ENDIAN);
|
||||
int[] findArray = new int[stringBuf.remaining() / 4];
|
||||
for (int i = 0; i < findArray.length; i++) {
|
||||
findArray[i] = stringBuf.getInt();
|
||||
}
|
||||
|
||||
ByteBuffer buf = ByteBuffer.wrap(fwbytes);
|
||||
buf.order(ByteOrder.BIG_ENDIAN);
|
||||
while (buf.remaining() > 3) {
|
||||
int arrayPos = 0;
|
||||
while (arrayPos < findArray.length && buf.remaining() > 3 && (buf.getInt() == findArray[arrayPos])) {
|
||||
arrayPos++;
|
||||
}
|
||||
if (arrayPos == findArray.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/* Copyright (C) 2022 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class Huami2021ChunkedDecoder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Huami2021ChunkedDecoder.class);
|
||||
|
||||
private Byte currentHandle;
|
||||
private int currentType;
|
||||
private int currentLength;
|
||||
ByteBuffer reassemblyBuffer;
|
||||
|
||||
private volatile byte[] sharedSessionKey;
|
||||
|
||||
private Huami2021Handler huami2021Handler;
|
||||
private final boolean force2021Protocol;
|
||||
|
||||
public Huami2021ChunkedDecoder(final Huami2021Handler huami2021Handler,
|
||||
final boolean force2021Protocol) {
|
||||
this.huami2021Handler = huami2021Handler;
|
||||
this.force2021Protocol = force2021Protocol;
|
||||
}
|
||||
|
||||
public void setEncryptionParameters(final byte[] sharedSessionKey) {
|
||||
this.sharedSessionKey = sharedSessionKey;
|
||||
}
|
||||
|
||||
public void setHuami2021Handler(final Huami2021Handler huami2021Handler) {
|
||||
this.huami2021Handler = huami2021Handler;
|
||||
}
|
||||
|
||||
public void decode(final byte[] data) {
|
||||
int i = 0;
|
||||
if (data[i++] != 0x03) {
|
||||
//LOG.warn("Ignoring non-chunked payload");
|
||||
return;
|
||||
}
|
||||
final byte flags = data[i++];
|
||||
final boolean encrypted = ((flags & 0x08) == 0x08);
|
||||
final boolean firstChunk = ((flags & 0x01) == 0x01);
|
||||
final boolean lastChunk = ((flags & 0x02) == 0x02);
|
||||
|
||||
if (force2021Protocol) {
|
||||
i++; // skip extended header
|
||||
}
|
||||
final byte handle = data[i++];
|
||||
if (currentHandle != null && currentHandle != handle) {
|
||||
LOG.warn("ignoring handle {}, expected {}", handle, currentHandle);
|
||||
return;
|
||||
}
|
||||
byte count = data[i++];
|
||||
if (firstChunk) { // beginning
|
||||
int full_length = (data[i++] & 0xff) | ((data[i++] & 0xff) << 8) | ((data[i++] & 0xff) << 16) | ((data[i++] & 0xff) << 24);
|
||||
currentLength = full_length;
|
||||
if (encrypted) {
|
||||
int encrypted_length = full_length + 8;
|
||||
int overflow = encrypted_length % 16;
|
||||
if (overflow > 0) {
|
||||
encrypted_length += (16 - overflow);
|
||||
}
|
||||
full_length = encrypted_length;
|
||||
}
|
||||
reassemblyBuffer = ByteBuffer.allocate(full_length);
|
||||
currentType = (data[i++] & 0xff) | ((data[i++] & 0xff) << 8);
|
||||
currentHandle = handle;
|
||||
}
|
||||
reassemblyBuffer.put(data, i, data.length - i);
|
||||
if (lastChunk) { // end
|
||||
byte[] buf = reassemblyBuffer.array();
|
||||
if (encrypted) {
|
||||
if (sharedSessionKey == null) {
|
||||
// Should never happen
|
||||
LOG.warn("Got encrypted message, but there's no shared session key");
|
||||
currentHandle = null;
|
||||
currentType = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] messagekey = new byte[16];
|
||||
for (int j = 0; j < 16; j++) {
|
||||
messagekey[j] = (byte) (sharedSessionKey[j] ^ handle);
|
||||
}
|
||||
try {
|
||||
buf = CryptoUtils.decryptAES(buf, messagekey);
|
||||
buf = ArrayUtils.subarray(buf, 0, currentLength);
|
||||
LOG.debug("decrypted data {}: {}", String.format("0x%04x", currentType), GB.hexdump(buf));
|
||||
} catch (Exception e) {
|
||||
LOG.warn("error decrypting " + e);
|
||||
currentHandle = null;
|
||||
currentType = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
huami2021Handler.handle2021Payload(currentType, buf);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to handle payload", e);
|
||||
}
|
||||
currentHandle = null;
|
||||
currentType = 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
/* Copyright (C) 2022 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
||||
|
||||
public class Huami2021ChunkedEncoder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Huami2021ChunkedEncoder.class);
|
||||
|
||||
private final BluetoothGattCharacteristic characteristicChunked2021Write;
|
||||
|
||||
private byte writeHandle;
|
||||
|
||||
// These must be volatile, since they are set by a different thread. Sometimes, GB might
|
||||
// attempt to encode a payload before they were set, which will make them not be propagated
|
||||
// to that thread later.
|
||||
private volatile int encryptedSequenceNr;
|
||||
private volatile byte[] sharedSessionKey;
|
||||
|
||||
private final boolean force2021Protocol;
|
||||
private volatile int mMTU = 23;
|
||||
|
||||
public Huami2021ChunkedEncoder(final BluetoothGattCharacteristic characteristicChunked2021Write,
|
||||
final boolean force2021Protocol,
|
||||
final int mMTU) {
|
||||
this.characteristicChunked2021Write = characteristicChunked2021Write;
|
||||
this.force2021Protocol = force2021Protocol;
|
||||
this.mMTU = mMTU;
|
||||
}
|
||||
|
||||
public synchronized void setEncryptionParameters(final int encryptedSequenceNr, final byte[] sharedSessionKey) {
|
||||
this.encryptedSequenceNr = encryptedSequenceNr;
|
||||
this.sharedSessionKey = sharedSessionKey;
|
||||
}
|
||||
|
||||
public synchronized void setMTU(int mMTU) {
|
||||
this.mMTU = mMTU;
|
||||
}
|
||||
|
||||
public synchronized void write(final TransactionBuilder builder,
|
||||
final short type,
|
||||
byte[] data,
|
||||
final boolean extended_flags,
|
||||
final boolean encrypt) {
|
||||
if (encrypt && sharedSessionKey == null) {
|
||||
LOG.error("Can't encrypt without the shared session key");
|
||||
return;
|
||||
}
|
||||
|
||||
writeHandle++;
|
||||
|
||||
int remaining = data.length;
|
||||
int length = data.length;
|
||||
byte count = 0;
|
||||
int header_size = 10;
|
||||
|
||||
if (extended_flags) {
|
||||
header_size++;
|
||||
}
|
||||
|
||||
if (extended_flags && encrypt) {
|
||||
byte[] messagekey = new byte[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
messagekey[i] = (byte) (sharedSessionKey[i] ^ writeHandle);
|
||||
}
|
||||
int encrypted_length = length + 8;
|
||||
int overflow = encrypted_length % 16;
|
||||
if (overflow > 0) {
|
||||
encrypted_length += (16 - overflow);
|
||||
}
|
||||
|
||||
byte[] encryptable_payload = new byte[encrypted_length];
|
||||
System.arraycopy(data, 0, encryptable_payload, 0, length);
|
||||
encryptable_payload[length] = (byte) (encryptedSequenceNr & 0xff);
|
||||
encryptable_payload[length + 1] = (byte) ((encryptedSequenceNr >> 8) & 0xff);
|
||||
encryptable_payload[length + 2] = (byte) ((encryptedSequenceNr >> 16) & 0xff);
|
||||
encryptable_payload[length + 3] = (byte) ((encryptedSequenceNr >> 24) & 0xff);
|
||||
encryptedSequenceNr++;
|
||||
int checksum = CheckSums.getCRC32(encryptable_payload, 0, length + 4);
|
||||
encryptable_payload[length + 4] = (byte) (checksum & 0xff);
|
||||
encryptable_payload[length + 5] = (byte) ((checksum >> 8) & 0xff);
|
||||
encryptable_payload[length + 6] = (byte) ((checksum >> 16) & 0xff);
|
||||
encryptable_payload[length + 7] = (byte) ((checksum >> 24) & 0xff);
|
||||
remaining = encrypted_length;
|
||||
try {
|
||||
data = CryptoUtils.encryptAES(encryptable_payload, messagekey);
|
||||
} catch (Exception e) {
|
||||
LOG.error("error while encrypting", e);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
while (remaining > 0) {
|
||||
int MAX_CHUNKLENGTH = mMTU - 3 - header_size;
|
||||
int copybytes = Math.min(remaining, MAX_CHUNKLENGTH);
|
||||
byte[] chunk = new byte[copybytes + header_size];
|
||||
|
||||
byte flags = 0;
|
||||
if (encrypt) {
|
||||
flags |= 0x08;
|
||||
}
|
||||
if (count == 0) {
|
||||
flags |= 0x01;
|
||||
int i = 4;
|
||||
if (extended_flags) {
|
||||
i++;
|
||||
}
|
||||
chunk[i++] = (byte) (length & 0xff);
|
||||
chunk[i++] = (byte) ((length >> 8) & 0xff);
|
||||
chunk[i++] = (byte) ((length >> 16) & 0xff);
|
||||
chunk[i++] = (byte) ((length >> 24) & 0xff);
|
||||
chunk[i++] = (byte) (type & 0xff);
|
||||
chunk[i] = (byte) ((type >> 8) & 0xff);
|
||||
}
|
||||
if (remaining <= MAX_CHUNKLENGTH) {
|
||||
flags |= 0x06; // last chunk?
|
||||
}
|
||||
chunk[0] = 0x03;
|
||||
chunk[1] = flags;
|
||||
if (extended_flags) {
|
||||
chunk[2] = 0;
|
||||
chunk[3] = writeHandle;
|
||||
chunk[4] = count;
|
||||
} else {
|
||||
chunk[2] = writeHandle;
|
||||
chunk[3] = count;
|
||||
}
|
||||
|
||||
System.arraycopy(data, data.length - remaining, chunk, header_size, copybytes);
|
||||
builder.write(characteristicChunked2021Write, chunk);
|
||||
remaining -= copybytes;
|
||||
header_size = 4;
|
||||
|
||||
if (extended_flags) {
|
||||
header_size++;
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,667 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import static org.apache.commons.lang3.ArrayUtils.subarray;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALWAYS_ON_DISPLAY_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALWAYS_ON_DISPLAY_MODE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALWAYS_ON_DISPLAY_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BT_CONNECTED_ADVERTISEMENT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DISPLAY_ON_LIFT_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DISPLAY_ON_LIFT_SENSITIVITY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DISPLAY_ON_LIFT_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HEARTRATE_ALERT_HIGH_THRESHOLD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HEARTRATE_ALERT_LOW_THRESHOLD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HEARTRATE_MEASUREMENT_INTERVAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HEARTRATE_STRESS_MONITORING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HEARTRATE_STRESS_RELAXATION_REMINDER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HEARTRATE_USE_FOR_SLEEP_DETECTION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_INACTIVITY_DND;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_INACTIVITY_DND_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_INACTIVITY_DND_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_INACTIVITY_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_INACTIVITY_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SCREEN_TIMEOUT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SPO2_ALL_DAY_MONITORING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SPO2_LOW_ALERT_THRESHOLD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_USER_FITNESS_GOAL_NOTIFICATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl.PREF_PASSWORD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl.PREF_PASSWORD_ENABLED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service.CHUNKED2021_ENDPOINT_CONFIG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service.CONFIG_CMD_SET;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_START;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.ActivateDisplayOnLift;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.ActivateDisplayOnLiftSensitivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.AlwaysOnDisplay;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.DoNotDisturb;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.MapUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class Huami2021Config {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Huami2021Config.class);
|
||||
|
||||
public enum ConfigType {
|
||||
DISPLAY(0x01, 0x02),
|
||||
LOCKSCREEN(0x04, 0x01),
|
||||
LANGUAGE(0x07, 0x02),
|
||||
HEALTH(0x08, 0x02),
|
||||
SYSTEM(0x0a, 0x01),
|
||||
BLUETOOTH(0x0b, 0x01),
|
||||
;
|
||||
|
||||
private final byte value;
|
||||
private final byte nextByte; // FIXME what does this byte mean?
|
||||
|
||||
ConfigType(int value, int nextByte) {
|
||||
this.value = (byte) value;
|
||||
this.nextByte = (byte) nextByte;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public byte getNextByte() {
|
||||
return nextByte;
|
||||
}
|
||||
|
||||
public static ConfigType fromValue(final byte value) {
|
||||
for (final ConfigType configType : values()) {
|
||||
if (configType.getValue() == value) {
|
||||
return configType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ArgType {
|
||||
BOOL(0x0b),
|
||||
STRING(0x20),
|
||||
SHORT(0x01),
|
||||
INT(0x03),
|
||||
BYTE(0x10),
|
||||
DATETIME_HH_MM(0x30),
|
||||
;
|
||||
|
||||
private final byte value;
|
||||
|
||||
ArgType(int value) {
|
||||
this.value = (byte) value;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConfigArg {
|
||||
// Display
|
||||
SCREEN_BRIGHTNESS(ConfigType.DISPLAY, ArgType.SHORT, 0x02, PREF_SCREEN_BRIGHTNESS),
|
||||
SCREEN_TIMEOUT(ConfigType.DISPLAY, ArgType.BYTE, 0x03, PREF_SCREEN_TIMEOUT),
|
||||
ALWAYS_ON_DISPLAY_MODE(ConfigType.DISPLAY, ArgType.BYTE, 0x04, PREF_ALWAYS_ON_DISPLAY_MODE),
|
||||
ALWAYS_ON_DISPLAY_SCHEDULED_START(ConfigType.DISPLAY, ArgType.DATETIME_HH_MM, 0x05, PREF_ALWAYS_ON_DISPLAY_START),
|
||||
ALWAYS_ON_DISPLAY_SCHEDULED_END(ConfigType.DISPLAY, ArgType.DATETIME_HH_MM, 0x06, PREF_ALWAYS_ON_DISPLAY_END),
|
||||
LIFT_WRIST_MODE(ConfigType.DISPLAY, ArgType.BYTE, 0x08, PREF_ACTIVATE_DISPLAY_ON_LIFT),
|
||||
LIFT_WRIST_SCHEDULED_START(ConfigType.DISPLAY, ArgType.DATETIME_HH_MM, 0x09, PREF_DISPLAY_ON_LIFT_START),
|
||||
LIFT_WRIST_SCHEDULED_END(ConfigType.DISPLAY, ArgType.DATETIME_HH_MM, 0x0a, PREF_DISPLAY_ON_LIFT_END),
|
||||
LIFT_WRIST_RESPONSE_SENSITIVITY(ConfigType.DISPLAY, ArgType.BYTE, 0x0b, PREF_DISPLAY_ON_LIFT_SENSITIVITY),
|
||||
SCREEN_ON_ON_NOTIFICATIONS(ConfigType.DISPLAY, ArgType.BOOL, 0x0c, PREF_SCREEN_ON_ON_NOTIFICATIONS),
|
||||
|
||||
// Lock Screen
|
||||
PASSWORD_ENABLED(ConfigType.LOCKSCREEN, ArgType.BOOL, 0x01, PREF_PASSWORD_ENABLED),
|
||||
PASSWORD_TEXT(ConfigType.LOCKSCREEN, ArgType.STRING, 0x02, PREF_PASSWORD),
|
||||
|
||||
// Language
|
||||
LANGUAGE(ConfigType.LANGUAGE, ArgType.BYTE, 0x01, PREF_LANGUAGE),
|
||||
LANGUAGE_FOLLOW_PHONE(ConfigType.LANGUAGE, ArgType.BOOL, 0x02, null),
|
||||
|
||||
// Health
|
||||
HEART_RATE_ALL_DAY_MONITORING(ConfigType.HEALTH, ArgType.BYTE, 0x01, PREF_HEARTRATE_MEASUREMENT_INTERVAL),
|
||||
HEART_RATE_HIGH_ALERTS(ConfigType.HEALTH, ArgType.BYTE, 0x02, PREF_HEARTRATE_ALERT_HIGH_THRESHOLD),
|
||||
HEART_RATE_LOW_ALERTS(ConfigType.HEALTH, ArgType.BYTE, 0x03, PREF_HEARTRATE_ALERT_LOW_THRESHOLD),
|
||||
THIRD_PARTY_HR_SHARING(ConfigType.HEALTH, ArgType.BOOL, 0x05, PREF_EXPOSE_HR_THIRDPARTY),
|
||||
SLEEP_HIGH_ACCURACY_MONITORING(ConfigType.HEALTH, ArgType.BOOL, 0x11, PREF_HEARTRATE_USE_FOR_SLEEP_DETECTION),
|
||||
SLEEP_BREATHING_QUALITY_MONITORING(ConfigType.HEALTH, ArgType.BOOL, 0x12, PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING),
|
||||
STRESS_MONITORING(ConfigType.HEALTH, ArgType.BOOL, 0x13, PREF_HEARTRATE_STRESS_MONITORING),
|
||||
STRESS_RELAXATION_REMINDER(ConfigType.HEALTH, ArgType.BOOL, 0x14, PREF_HEARTRATE_STRESS_RELAXATION_REMINDER),
|
||||
SPO2_ALL_DAY_MONITORING(ConfigType.HEALTH, ArgType.BOOL, 0x31, PREF_SPO2_ALL_DAY_MONITORING),
|
||||
SPO2_LOW_ALERT(ConfigType.HEALTH, ArgType.BYTE, 0x32, PREF_SPO2_LOW_ALERT_THRESHOLD),
|
||||
FITNESS_GOAL_NOTIFICATION(ConfigType.HEALTH, ArgType.BOOL, 0x51, PREF_USER_FITNESS_GOAL_NOTIFICATION), // TODO is it?
|
||||
FITNESS_GOAL_STEPS(ConfigType.HEALTH, ArgType.INT, 0x52, null), // TODO needs to be handled globally
|
||||
INACTIVITY_WARNINGS_ENABLED(ConfigType.HEALTH, ArgType.BOOL, 0x41, PREF_INACTIVITY_ENABLE),
|
||||
INACTIVITY_WARNINGS_SCHEDULED_START(ConfigType.HEALTH, ArgType.DATETIME_HH_MM, 0x42, PREF_INACTIVITY_START),
|
||||
INACTIVITY_WARNINGS_SCHEDULED_END(ConfigType.HEALTH, ArgType.DATETIME_HH_MM, 0x43, PREF_INACTIVITY_END),
|
||||
INACTIVITY_WARNINGS_DND_ENABLED(ConfigType.HEALTH, ArgType.BOOL, 0x44, PREF_INACTIVITY_DND),
|
||||
INACTIVITY_WARNINGS_DND_SCHEDULED_START(ConfigType.HEALTH, ArgType.DATETIME_HH_MM, 0x45, PREF_INACTIVITY_DND_START),
|
||||
INACTIVITY_WARNINGS_DND_SCHEDULED_END(ConfigType.HEALTH, ArgType.DATETIME_HH_MM, 0x46, PREF_INACTIVITY_DND_END),
|
||||
|
||||
// System
|
||||
TIME_FORMAT(ConfigType.SYSTEM, ArgType.BYTE, 0x01, PREF_TIMEFORMAT),
|
||||
DATE_FORMAT(ConfigType.SYSTEM, ArgType.STRING, 0x02, null),
|
||||
DND_MODE(ConfigType.SYSTEM, ArgType.BYTE, 0x0a, PREF_DO_NOT_DISTURB),
|
||||
DND_SCHEDULED_START(ConfigType.SYSTEM, ArgType.DATETIME_HH_MM, 0x0b, PREF_DO_NOT_DISTURB_START),
|
||||
DND_SCHEDULED_END(ConfigType.SYSTEM, ArgType.DATETIME_HH_MM, 0x0c, PREF_DO_NOT_DISTURB_END),
|
||||
TEMPERATURE_UNIT(ConfigType.SYSTEM, ArgType.BYTE, 0x12, null),
|
||||
TIME_FORMAT_FOLLOWS_PHONE(ConfigType.SYSTEM, ArgType.BOOL, 0x13, null),
|
||||
DISPLAY_CALLER(ConfigType.SYSTEM, ArgType.BOOL, 0x18, null), // TODO Handle
|
||||
NIGHT_MODE_MODE(ConfigType.SYSTEM, ArgType.BYTE, 0x1b, PREF_NIGHT_MODE),
|
||||
NIGHT_MODE_SCHEDULED_START(ConfigType.SYSTEM, ArgType.DATETIME_HH_MM, 0x1c, PREF_NIGHT_MODE_START),
|
||||
NIGHT_MODE_SCHEDULED_END(ConfigType.SYSTEM, ArgType.DATETIME_HH_MM, 0x1d, PREF_NIGHT_MODE_END),
|
||||
|
||||
// Bluetooth
|
||||
BLUETOOTH_CONNECTED_ADVERTISING(ConfigType.BLUETOOTH, ArgType.BOOL, 0x02, PREF_BT_CONNECTED_ADVERTISEMENT),
|
||||
;
|
||||
|
||||
private final ConfigType configType;
|
||||
private final ArgType argType;
|
||||
private final byte code;
|
||||
private final String prefKey;
|
||||
|
||||
ConfigArg(final ConfigType configType, final ArgType argType, final int code, final String prefKey) {
|
||||
this.configType = configType;
|
||||
this.argType = argType;
|
||||
this.code = (byte) code;
|
||||
this.prefKey = prefKey;
|
||||
}
|
||||
|
||||
public ConfigType getConfigType() {
|
||||
return configType;
|
||||
}
|
||||
|
||||
public ArgType getArgType() {
|
||||
return argType;
|
||||
}
|
||||
|
||||
public byte getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getPrefKey() {
|
||||
return prefKey;
|
||||
}
|
||||
|
||||
public static ConfigArg fromCode(final ConfigType configType, final byte code) {
|
||||
for (final Huami2021Config.ConfigArg arg : values()) {
|
||||
if (arg.getConfigType().equals(configType) && arg.getCode() == code) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<ConfigArg> getAllArgsForConfigType(final ConfigType configType) {
|
||||
final List<Huami2021Config.ConfigArg> configArgs = new ArrayList<>();
|
||||
for (final Huami2021Config.ConfigArg arg : values()) {
|
||||
if (arg.getConfigType().equals(configType)) {
|
||||
configArgs.add(arg);
|
||||
}
|
||||
}
|
||||
return configArgs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConfigSetter {
|
||||
private final ConfigType configType;
|
||||
private final Map<ConfigArg, byte[]> arguments = new LinkedHashMap<>();
|
||||
|
||||
public ConfigSetter(final ConfigType configType) {
|
||||
this.configType = configType;
|
||||
}
|
||||
|
||||
public ConfigSetter setBoolean(final ConfigArg arg, final boolean value) {
|
||||
checkArg(arg, ArgType.BOOL);
|
||||
|
||||
arguments.put(arg, new byte[]{(byte) (value ? 0x01 : 0x00)});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigSetter setString(final ConfigArg arg, final String value) {
|
||||
checkArg(arg, ArgType.STRING);
|
||||
|
||||
arguments.put(arg, (value + "\0").getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigSetter setShort(final ConfigArg arg, final short value) {
|
||||
checkArg(arg, ArgType.SHORT);
|
||||
|
||||
arguments.put(arg, BLETypeConversions.fromUint16(value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigSetter setInt(final ConfigArg arg, final int value) {
|
||||
checkArg(arg, ArgType.INT);
|
||||
|
||||
arguments.put(arg, BLETypeConversions.fromUint32(value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigSetter setByte(final ConfigArg arg, final byte value) {
|
||||
checkArg(arg, ArgType.BYTE);
|
||||
|
||||
arguments.put(arg, new byte[]{value});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigSetter setHourMinute(final ConfigArg arg, final Date date) {
|
||||
checkArg(arg, ArgType.DATETIME_HH_MM);
|
||||
|
||||
final Calendar calendar = GregorianCalendar.getInstance();
|
||||
calendar.setTime(date);
|
||||
|
||||
arguments.put(arg, new byte[]{
|
||||
(byte) calendar.get(Calendar.HOUR_OF_DAY),
|
||||
(byte) calendar.get(Calendar.MINUTE)
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public byte[] encode() {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
baos.write(CONFIG_CMD_SET);
|
||||
baos.write(configType.getValue());
|
||||
baos.write(configType.getNextByte());
|
||||
baos.write(0x00); // ?
|
||||
baos.write(arguments.size());
|
||||
for (final Map.Entry<ConfigArg, byte[]> arg : arguments.entrySet()) {
|
||||
final ArgType argType = arg.getKey().getArgType();
|
||||
baos.write(arg.getKey().getCode());
|
||||
baos.write(argType.getValue());
|
||||
baos.write(arg.getValue());
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to encode command", e);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public void write(final Huami2021Support support, final TransactionBuilder builder) {
|
||||
support.writeToChunked2021(builder, CHUNKED2021_ENDPOINT_CONFIG, encode(), true);
|
||||
}
|
||||
|
||||
public void write(final Huami2021Support support) {
|
||||
try {
|
||||
final TransactionBuilder builder = support.performInitialized("write config");
|
||||
support.writeToChunked2021(builder, CHUNKED2021_ENDPOINT_CONFIG, encode(), true);
|
||||
builder.queue(support.getQueue());
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to write config", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkArg(final ConfigArg arg, final ArgType expectedArgType) {
|
||||
try {
|
||||
if (!configType.equals(arg.getConfigType())) {
|
||||
throw new IllegalArgumentException("Unexpected config type " + arg.getConfigType());
|
||||
}
|
||||
|
||||
if (!expectedArgType.equals(arg.getArgType())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"Invalid arg type %s for %s, expected %s",
|
||||
expectedArgType,
|
||||
arg,
|
||||
arg.getArgType()
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (final IllegalArgumentException e) {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
// Crash
|
||||
throw e;
|
||||
} else {
|
||||
LOG.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConfigParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ConfigParser.class);
|
||||
|
||||
private final ConfigType configType;
|
||||
|
||||
public ConfigParser(final ConfigType configType) {
|
||||
this.configType = configType;
|
||||
}
|
||||
|
||||
public Map<String, Object> parse(final int expectedNumConfigs, final byte[] bytes) {
|
||||
final Map<String, Object> prefs = new HashMap<>();
|
||||
|
||||
int configCount = 0;
|
||||
int pos = 0;
|
||||
|
||||
while (pos < bytes.length) {
|
||||
if (configCount > expectedNumConfigs) {
|
||||
LOG.error("Got more configs than {}", expectedNumConfigs);
|
||||
return null;
|
||||
}
|
||||
|
||||
final Huami2021Config.ConfigArg configArg = Huami2021Config.ConfigArg.fromCode(configType, bytes[pos]);
|
||||
if (configArg == null) {
|
||||
LOG.error("Unknown config {} for {} at {}", String.format("0x%02x", bytes[pos]), configType, pos);
|
||||
return null;
|
||||
}
|
||||
|
||||
pos++;
|
||||
|
||||
final boolean unexpectedType = (bytes[pos] != configArg.getArgType().getValue());
|
||||
if (unexpectedType) {
|
||||
LOG.warn("Unexpected arg type {} for {}, expected {}", String.format("0x%02x", bytes[pos]), configArg, configArg.getArgType());
|
||||
}
|
||||
|
||||
pos++;
|
||||
|
||||
final Map<String, Object> argPrefs;
|
||||
|
||||
switch (configArg.getArgType()) {
|
||||
case BOOL:
|
||||
final boolean valBoolean = bytes[pos] == 1;
|
||||
LOG.info("Got {} for {} = {}", configArg.getArgType(), configArg, valBoolean);
|
||||
argPrefs = convertBooleanToPrefs(configArg, valBoolean);
|
||||
pos += 1;
|
||||
break;
|
||||
case STRING:
|
||||
final String valString = StringUtils.untilNullTerminator(bytes, pos);
|
||||
LOG.info("Got {} for {} = {}", configArg.getArgType(), configArg, valString);
|
||||
argPrefs = convertStringToPrefs(configArg, valString);
|
||||
pos += valString.length() + 1;
|
||||
break;
|
||||
case SHORT:
|
||||
final int valShort = BLETypeConversions.toUint16(subarray(bytes, pos, pos + 2));
|
||||
LOG.info("Got {} for {} = {}", configArg.getArgType(), configArg, valShort);
|
||||
argPrefs = convertNumberToPrefs(configArg, valShort);
|
||||
pos += 2;
|
||||
break;
|
||||
case INT:
|
||||
final int valInt = BLETypeConversions.toUint32(subarray(bytes, pos, pos + 4));
|
||||
LOG.info("Got {} for {} = {}", configArg.getArgType(), configArg, valInt);
|
||||
argPrefs = convertNumberToPrefs(configArg, valInt);
|
||||
pos += 4;
|
||||
break;
|
||||
case BYTE:
|
||||
final byte valByte = bytes[pos];
|
||||
LOG.info("Got {} for {} = {}", configArg.getArgType(), configArg, valByte);
|
||||
argPrefs = convertByteToPrefs(configArg, valByte);
|
||||
pos += 1;
|
||||
break;
|
||||
case DATETIME_HH_MM:
|
||||
final DateFormat df = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||
final String hhmm = String.format(Locale.ROOT, "%02d:%02d", bytes[pos], bytes[pos + 1]);
|
||||
try {
|
||||
df.parse(hhmm);
|
||||
} catch (final ParseException e) {
|
||||
LOG.error("Failed to parse HH:mm from {}", hhmm);
|
||||
return null;
|
||||
}
|
||||
LOG.info("Got {} for {} = {}", configArg.getArgType(), configArg, hhmm);
|
||||
argPrefs = convertDatetimeHhMmToPrefs(configArg, hhmm);
|
||||
pos += 2;
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unknown arg type {}", configArg);
|
||||
configCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argPrefs != null && !unexpectedType) {
|
||||
// Special cases for "follow phone" preferences. We need to ensure that "auto"
|
||||
// always has precedence
|
||||
if (argPrefs.containsKey(PREF_LANGUAGE) && prefs.containsKey(PREF_LANGUAGE)) {
|
||||
if (prefs.get(PREF_LANGUAGE).equals(DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO)) {
|
||||
argPrefs.remove(PREF_LANGUAGE);
|
||||
}
|
||||
}
|
||||
if (argPrefs.containsKey(PREF_TIMEFORMAT) && prefs.containsKey(PREF_TIMEFORMAT)) {
|
||||
if (prefs.get(PREF_TIMEFORMAT).equals(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_AUTO)) {
|
||||
argPrefs.remove(PREF_TIMEFORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
prefs.putAll(argPrefs);
|
||||
}
|
||||
|
||||
configCount++;
|
||||
}
|
||||
|
||||
return prefs;
|
||||
}
|
||||
|
||||
private static Map<String, Object> convertBooleanToPrefs(final ConfigArg configArg, final boolean value) {
|
||||
if (configArg.getPrefKey() != null) {
|
||||
// The arg maps to a boolean pref directly
|
||||
return singletonMap(configArg.getPrefKey(), value);
|
||||
}
|
||||
|
||||
switch(configArg) {
|
||||
case LANGUAGE_FOLLOW_PHONE:
|
||||
if (value) {
|
||||
return singletonMap(PREF_LANGUAGE, DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO);
|
||||
} else {
|
||||
// If not following phone, we'll receive the actual value in LANGUAGE
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
case TIME_FORMAT_FOLLOWS_PHONE:
|
||||
if (value) {
|
||||
return singletonMap(PREF_TIMEFORMAT, DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_AUTO);
|
||||
} else {
|
||||
// If not following phone, we'll receive the actual value in TIME_FORMAT
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG.warn("Unhandled Boolean pref {}", configArg);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> convertStringToPrefs(final ConfigArg configArg, final String str) {
|
||||
if (configArg.getPrefKey() != null) {
|
||||
// The arg maps to a string pref directly
|
||||
return singletonMap(configArg.getPrefKey(), str);
|
||||
}
|
||||
|
||||
switch(configArg) {
|
||||
case DATE_FORMAT:
|
||||
return singletonMap(PREF_DATEFORMAT, str.replace(".", "/").toUpperCase(Locale.ROOT));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG.warn("Unhandled String pref {}", configArg);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> convertNumberToPrefs(final ConfigArg configArg, final int value) {
|
||||
if (configArg.getPrefKey() != null) {
|
||||
// The arg maps to a number pref directly
|
||||
return singletonMap(configArg.getPrefKey(), value);
|
||||
}
|
||||
|
||||
LOG.warn("Unhandled number pref {}", configArg);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> convertDatetimeHhMmToPrefs(final ConfigArg configArg, final String hhmm) {
|
||||
if (configArg.getPrefKey() != null) {
|
||||
// The arg maps to a hhmm pref directly
|
||||
return singletonMap(configArg.getPrefKey(), hhmm);
|
||||
}
|
||||
|
||||
LOG.warn("Unhandled datetime pref {}", configArg);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> convertByteToPrefs(final ConfigArg configArg, final byte b) {
|
||||
switch(configArg) {
|
||||
case ALWAYS_ON_DISPLAY_MODE:
|
||||
switch(b) {
|
||||
case 0x00:
|
||||
return singletonMap(configArg.getPrefKey(), AlwaysOnDisplay.OFF.name().toLowerCase(Locale.ROOT));
|
||||
case 0x01:
|
||||
return singletonMap(configArg.getPrefKey(), AlwaysOnDisplay.AUTO.name().toLowerCase(Locale.ROOT));
|
||||
case 0x02:
|
||||
return singletonMap(configArg.getPrefKey(), AlwaysOnDisplay.SCHEDULED.name().toLowerCase(Locale.ROOT));
|
||||
case 0x03:
|
||||
return singletonMap(configArg.getPrefKey(), AlwaysOnDisplay.ALWAYS.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
break;
|
||||
case LIFT_WRIST_MODE:
|
||||
switch(b) {
|
||||
case 0x00:
|
||||
return singletonMap(configArg.getPrefKey(), ActivateDisplayOnLift.OFF.name().toLowerCase(Locale.ROOT));
|
||||
case 0x01:
|
||||
return singletonMap(configArg.getPrefKey(), ActivateDisplayOnLift.SCHEDULED.name().toLowerCase(Locale.ROOT));
|
||||
case 0x02:
|
||||
return singletonMap(configArg.getPrefKey(), ActivateDisplayOnLift.ON.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
break;
|
||||
case LIFT_WRIST_RESPONSE_SENSITIVITY:
|
||||
switch(b) {
|
||||
case 0x00:
|
||||
return singletonMap(configArg.getPrefKey(), ActivateDisplayOnLiftSensitivity.NORMAL.name().toLowerCase(Locale.ROOT));
|
||||
case 0x01:
|
||||
return singletonMap(configArg.getPrefKey(), ActivateDisplayOnLiftSensitivity.SENSITIVE.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
break;
|
||||
case LANGUAGE:
|
||||
final Map<Integer, String> reverseLanguageLookup = MapUtils.reverse(HuamiLanguageType.idLookup);
|
||||
final String language = reverseLanguageLookup.get(b & 0xff);
|
||||
if (language != null) {
|
||||
return singletonMap(configArg.getPrefKey(), language);
|
||||
}
|
||||
break;
|
||||
case HEART_RATE_ALL_DAY_MONITORING:
|
||||
if (b > 0) {
|
||||
return singletonMap(configArg.getPrefKey(), String.format("%d", (b & 0xff) * 60));
|
||||
} else {
|
||||
return singletonMap(configArg.getPrefKey(), String.format("%d", b));
|
||||
}
|
||||
case SCREEN_TIMEOUT:
|
||||
case HEART_RATE_HIGH_ALERTS:
|
||||
case HEART_RATE_LOW_ALERTS:
|
||||
case SPO2_LOW_ALERT:
|
||||
return singletonMap(configArg.getPrefKey(), String.format("%d", b & 0xff));
|
||||
case TIME_FORMAT:
|
||||
switch(b) {
|
||||
case 0x00:
|
||||
return singletonMap(configArg.getPrefKey(), DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H);
|
||||
case 0x01:
|
||||
return singletonMap(configArg.getPrefKey(), DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H);
|
||||
}
|
||||
break;
|
||||
case DND_MODE:
|
||||
switch(b) {
|
||||
case 0x00:
|
||||
return singletonMap(configArg.getPrefKey(), DoNotDisturb.OFF.name().toLowerCase(Locale.ROOT));
|
||||
case 0x01:
|
||||
return singletonMap(configArg.getPrefKey(), DoNotDisturb.SCHEDULED.name().toLowerCase(Locale.ROOT));
|
||||
case 0x02:
|
||||
return singletonMap(configArg.getPrefKey(), DoNotDisturb.AUTOMATIC.name().toLowerCase(Locale.ROOT));
|
||||
case 0x03:
|
||||
return singletonMap(configArg.getPrefKey(), DoNotDisturb.ALWAYS.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
break;
|
||||
case TEMPERATURE_UNIT:
|
||||
// TODO: This should be per device...
|
||||
//switch(b) {
|
||||
// case 0x00:
|
||||
// return singletonMap(SettingsActivity.PREF_MEASUREMENT_SYSTEM, METRIC);
|
||||
// case 0x01:
|
||||
// return singletonMap(SettingsActivity.PREF_MEASUREMENT_SYSTEM, IMPERIAL);
|
||||
//}
|
||||
break;
|
||||
case NIGHT_MODE_MODE:
|
||||
switch(b) {
|
||||
case 0x00:
|
||||
return singletonMap(configArg.getPrefKey(), MiBandConst.PREF_NIGHT_MODE_OFF);
|
||||
case 0x01:
|
||||
return singletonMap(configArg.getPrefKey(), MiBandConst.PREF_NIGHT_MODE_SUNSET);
|
||||
case 0x02:
|
||||
return singletonMap(configArg.getPrefKey(), MiBandConst.PREF_NIGHT_MODE_SCHEDULED);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG.warn("Unhandled byte pref {}", configArg);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> singletonMap(final String key, final Object value) {
|
||||
if (key == null && BuildConfig.DEBUG) {
|
||||
// Crash
|
||||
throw new IllegalStateException("Null key in prefs update");
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,265 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public abstract class Huami2021FirmwareInfo extends AbstractHuamiFirmwareInfo {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Huami2021FirmwareInfo.class);
|
||||
|
||||
public static final byte[] ZIP_HEADER = new byte[]{
|
||||
0x50, 0x4B, 0x03, 0x04
|
||||
};
|
||||
|
||||
public static final byte[] FW_HEADER = new byte[]{
|
||||
0x51, 0x71
|
||||
};
|
||||
|
||||
public Huami2021FirmwareInfo(final byte[] bytes) {
|
||||
super(bytes);
|
||||
}
|
||||
|
||||
public abstract String deviceName();
|
||||
|
||||
@Override
|
||||
protected HuamiFirmwareType determineFirmwareType(final byte[] bytes) {
|
||||
if (ArrayUtils.equals(bytes, UIHHContainer.UIHH_HEADER, 0)) {
|
||||
final UIHHContainer uihh = UIHHContainer.fromRawBytes(bytes);
|
||||
if (uihh == null) {
|
||||
LOG.warn("Invalid UIHH file");
|
||||
return HuamiFirmwareType.INVALID;
|
||||
}
|
||||
|
||||
UIHHContainer.FileEntry uihhFirmwareZipFile = null;
|
||||
boolean hasChangelog = false;
|
||||
for (final UIHHContainer.FileEntry file : uihh.getFiles()) {
|
||||
switch(file.getType()) {
|
||||
case FIRMWARE_ZIP:
|
||||
uihhFirmwareZipFile = file;
|
||||
continue;
|
||||
case FIRMWARE_CHANGELOG:
|
||||
hasChangelog = true;
|
||||
continue;
|
||||
default:
|
||||
LOG.warn("Unexpected file for {}", file.getType());
|
||||
}
|
||||
}
|
||||
|
||||
if (uihhFirmwareZipFile != null && hasChangelog) {
|
||||
final byte[] firmwareBin = getFileFromZip(uihhFirmwareZipFile.getContent(), "META/firmware.bin");
|
||||
|
||||
if (isCompatibleFirmwareBin(firmwareBin)) {
|
||||
// TODO: Firmware upgrades are untested, so they are disabled
|
||||
return HuamiFirmwareType.INVALID;
|
||||
//return HuamiFirmwareType.FIRMWARE_UIHH_2021_ZIP_WITH_CHANGELOG;
|
||||
}
|
||||
}
|
||||
|
||||
return HuamiFirmwareType.INVALID;
|
||||
}
|
||||
|
||||
if (!ArrayUtils.equals(bytes, ZIP_HEADER, 0)) {
|
||||
return HuamiFirmwareType.INVALID;
|
||||
}
|
||||
|
||||
final byte[] firmwareBin = getFileFromZip(bytes, "META/firmware.bin");
|
||||
if (isCompatibleFirmwareBin(firmwareBin)) {
|
||||
// TODO: Firmware upgrades are untested, so they are disabled
|
||||
return HuamiFirmwareType.INVALID;
|
||||
//return HuamiFirmwareType.FIRMWARE;
|
||||
}
|
||||
|
||||
final String appType = getAppType();
|
||||
if ("watchface".equals(appType)) {
|
||||
return HuamiFirmwareType.WATCHFACE;
|
||||
}
|
||||
|
||||
return HuamiFirmwareType.INVALID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toVersion(int crc16) {
|
||||
final String crcMapVersion = getCrcMap().get(crc16);
|
||||
if (crcMapVersion != null) {
|
||||
return crcMapVersion;
|
||||
}
|
||||
|
||||
switch (firmwareType) {
|
||||
case FIRMWARE_UIHH_2021_ZIP_WITH_CHANGELOG:
|
||||
final UIHHContainer uihh = UIHHContainer.fromRawBytes(getBytes());
|
||||
if (uihh == null) {
|
||||
return null;
|
||||
}
|
||||
return getFirmwareVersion(uihh.getFile(UIHHContainer.FileType.FIRMWARE_ZIP));
|
||||
case FIRMWARE:
|
||||
return getFirmwareVersion(getBytes());
|
||||
case WATCHFACE:
|
||||
final String appName = getAppName();
|
||||
if (appName == null) {
|
||||
return "(unknown watchface)";
|
||||
}
|
||||
return String.format("%s (watchface)", appName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isCompatibleFirmwareBin(final byte[] firmwareBin) {
|
||||
if (firmwareBin == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ArrayUtils.equals(firmwareBin, FW_HEADER, 0)) {
|
||||
LOG.warn("Unexpected firmware header: {}", GB.hexdump(Arrays.copyOfRange(firmwareBin, 0, FW_HEADER.length + 1)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// On the MB7, this only works for firmwares > 1.8.5.1, not for any older firmware
|
||||
if (!searchString32BitAligned(firmwareBin, deviceName())) {
|
||||
LOG.warn("Failed to find {} in fwBytes", deviceName());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getFirmwareVersion(final byte[] fwbytes) {
|
||||
final byte[] firmwareBin = getFileFromZip(fwbytes, "META/firmware.bin");
|
||||
|
||||
if (firmwareBin == null) {
|
||||
LOG.warn("Failed to read firmware.bin");
|
||||
return null;
|
||||
}
|
||||
|
||||
int startIdx = 10;
|
||||
int endIdx = -1;
|
||||
|
||||
for (int i = startIdx; i < startIdx + 20; i++) {
|
||||
byte c = firmwareBin[i];
|
||||
|
||||
if (c == 0) {
|
||||
endIdx = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (c != '.' && (c < '0' || c > '9')) {
|
||||
// not a valid version character
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (endIdx == -1) {
|
||||
LOG.warn("Failed to find firmware version in expected offset");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new String(Arrays.copyOfRange(firmwareBin, startIdx, endIdx));
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
final byte[] appJsonBin = getFileFromZip(getBytes(), "app.json");
|
||||
if (appJsonBin == null) {
|
||||
LOG.warn("Failed to get app.json from zip");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final String appJsonString = new String(appJsonBin, StandardCharsets.UTF_8)
|
||||
// Remove UTF-8 BOM if present
|
||||
.replace("\uFEFF", "");
|
||||
final JSONObject jsonObject = new JSONObject(appJsonString);
|
||||
// TODO check i18n section?
|
||||
// TODO Show preview icon?
|
||||
final String appName = jsonObject.getJSONObject("app").getString("appName");
|
||||
|
||||
return String.format("%s (watchface)", appName);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse app.json", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getAppType() {
|
||||
final byte[] appJsonBin = getFileFromZip(getBytes(), "app.json");
|
||||
if (appJsonBin == null) {
|
||||
LOG.warn("Failed to get app.json from zip");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final String appJsonString = new String(appJsonBin, StandardCharsets.UTF_8)
|
||||
// Remove UTF-8 BOM if present
|
||||
.replace("\uFEFF", "");
|
||||
final JSONObject jsonObject = new JSONObject(appJsonString);
|
||||
return jsonObject.getJSONObject("app").getString("appType");
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse app.json", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] getFileFromZip(final byte[] zipBytes, final String path) {
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||
ZipEntry zipEntry;
|
||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
||||
if (zipEntry.getName().equals(path)) {
|
||||
return readAllBytes(zipInputStream);
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
LOG.error(String.format("Failed to read %s from zip", path), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.debug("{} not found in zip", path);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] readAllBytes(final InputStream is) throws IOException {
|
||||
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
|
||||
int n;
|
||||
byte[] buf = new byte[16384];
|
||||
|
||||
while ((n = is.read(buf, 0, buf.length)) != -1) {
|
||||
buffer.write(buf, 0, n);
|
||||
}
|
||||
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
public interface Huami2021Handler {
|
||||
void handle2021Payload(int type, byte[] payload);
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Huami2021MenuType {
|
||||
/**
|
||||
* These somewhat match the ones in {@link HuamiMenuType}, but not all. The band sends and
|
||||
* receives those as 8-digit upper case hex strings.
|
||||
*/
|
||||
public static final Map<String, Integer> displayItemIdLookup = new HashMap<String, Integer>() {{
|
||||
put("personal_activity_intelligence", 0x01);
|
||||
put("hr", 0x02);
|
||||
put("workout", 0x03);
|
||||
put("weather", 0x04);
|
||||
put("alarm", 0x09);
|
||||
put("worldclock", 0x1A);
|
||||
put("music", 0x0B);
|
||||
put("stopwatch", 0x0C);
|
||||
put("countdown", 0x0D);
|
||||
put("findphone", 0x0E);
|
||||
put("mutephone", 0x0F);
|
||||
put("settings", 0x13);
|
||||
put("workout_history", 0x14);
|
||||
put("eventreminder", 0x15);
|
||||
put("pai", 0x19);
|
||||
put("takephoto", 0x0A);
|
||||
put("stress", 0x1C);
|
||||
put("female_health", 0x1D);
|
||||
put("workout_status", 0x1E);
|
||||
put("sleep", 0x23);
|
||||
put("spo2", 0x24);
|
||||
put("events", 0x26);
|
||||
put("breathing", 0x33);
|
||||
put("pomodoro", 0x38);
|
||||
put("flashlight", 0x0102);
|
||||
}};
|
||||
|
||||
public static final Map<String, Integer> shortcutsIdLookup = new HashMap<String, Integer>() {{
|
||||
put("hr", 0x01);
|
||||
put("workout", 0x0A);
|
||||
put("workout_status", 0x0C);
|
||||
put("weather", 0x02);
|
||||
put("worldclock", 0x1A);
|
||||
put("alarm", 0x16);
|
||||
put("music", 0x04);
|
||||
put("activity", 0x20);
|
||||
put("eventreminder", 0x21);
|
||||
put("female_health", 0x11);
|
||||
put("pai", 0x03);
|
||||
put("stress", 0x0F);
|
||||
put("sleep", 0x05);
|
||||
put("spo2", 0x13);
|
||||
put("events", 0x18);
|
||||
put("breathing", 0x12);
|
||||
}};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,326 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
import net.e175.klaus.solarpositioning.DeltaT;
|
||||
import net.e175.klaus.solarpositioning.SPA;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.threeten.bp.LocalDate;
|
||||
import org.threeten.bp.format.DateTimeFormatter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
|
||||
|
||||
/**
|
||||
* The weather models that the bands expect as an http response to weather requests. Base URL usually
|
||||
* is https://api-mifit.huami.com.
|
||||
*/
|
||||
public class Huami2021Weather {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Huami2021Weather.class);
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.serializeNulls()
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") // for pubTimes
|
||||
.registerTypeAdapter(LocalDate.class, new LocalDateSerializer())
|
||||
.create();
|
||||
|
||||
public static Response handleHttpRequest(final String path, final Map<String, String> query) {
|
||||
final WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
|
||||
|
||||
if (weatherSpec == null) {
|
||||
LOG.error("No weather in weather instance");
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (path) {
|
||||
case "/weather/v2/forecast":
|
||||
final String daysStr = query.get("days");
|
||||
final int days;
|
||||
if (daysStr != null) {
|
||||
days = Integer.parseInt(daysStr);
|
||||
} else {
|
||||
days = 10;
|
||||
}
|
||||
return new ForecastResponse(weatherSpec, days);
|
||||
case "/weather/index":
|
||||
return new IndexResponse(weatherSpec);
|
||||
case "/weather/current":
|
||||
return new CurrentResponse(weatherSpec);
|
||||
case "/weather/forecast/hourly":
|
||||
return new HourlyResponse();
|
||||
case "/weather/alerts":
|
||||
return new AlertsResponse();
|
||||
default:
|
||||
LOG.error("Unknown weather path {}", path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class RawJsonStringResponse extends Response {
|
||||
private final String content;
|
||||
|
||||
public RawJsonStringResponse(final String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Response {
|
||||
public String toJson() {
|
||||
return GSON.toJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
// /weather/v2/forecast
|
||||
//
|
||||
// locale=zh_CN
|
||||
// deviceSource=11
|
||||
// days=10
|
||||
// isGlobal=true
|
||||
// locationKey=00.000,-0.000,xiaomi_accu:000000
|
||||
public static class ForecastResponse extends Response {
|
||||
public Date pubTime;
|
||||
public List<Object> humidity = new ArrayList<>();
|
||||
public List<Range> temperature = new ArrayList<>();
|
||||
public List<Range> weather = new ArrayList<>();
|
||||
public List<Range> windDirection = new ArrayList<>();
|
||||
public List<Range> sunRiseSet = new ArrayList<>();
|
||||
public List<Range> windSpeed = new ArrayList<>();
|
||||
public List<Object> moonRiseSet = new ArrayList<>();
|
||||
public List<Object> airQualities = new ArrayList<>();
|
||||
|
||||
public ForecastResponse(final WeatherSpec weatherSpec, final int days) {
|
||||
pubTime = new Date(weatherSpec.timestamp * 1000L);
|
||||
|
||||
final Calendar calendar = GregorianCalendar.getInstance();
|
||||
calendar.setTime(new Date(weatherSpec.timestamp * 1000L));
|
||||
|
||||
// TODO: We should send sunrise on the same location as the weather
|
||||
final SimpleDateFormat sunRiseSetSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT);
|
||||
final Location lastKnownLocation = new CurrentPosition().getLastKnownLocation();
|
||||
final GregorianCalendar sunriseDate = new GregorianCalendar();
|
||||
sunriseDate.setTime(calendar.getTime());
|
||||
|
||||
for (int i = 0; i < Math.min(weatherSpec.forecasts.size(), days); i++) {
|
||||
final WeatherSpec.Forecast forecast = weatherSpec.forecasts.get(i);
|
||||
temperature.add(new Range(forecast.minTemp - 273, forecast.maxTemp - 273));
|
||||
final String weatherCode = String.valueOf(HuamiWeatherConditions.mapToAmazfitBipWeatherCode(forecast.conditionCode) & 0xff); // is it?
|
||||
weather.add(new Range(weatherCode, weatherCode));
|
||||
|
||||
final GregorianCalendar[] sunriseTransitSet = SPA.calculateSunriseTransitSet(
|
||||
sunriseDate,
|
||||
lastKnownLocation.getLatitude(),
|
||||
lastKnownLocation.getLongitude(),
|
||||
DeltaT.estimate(sunriseDate)
|
||||
);
|
||||
|
||||
final String from = sunRiseSetSdf.format(sunriseTransitSet[0].getTime());
|
||||
final String to = sunRiseSetSdf.format(sunriseTransitSet[2].getTime());
|
||||
sunRiseSet.add(new Range(from, to));
|
||||
|
||||
sunriseDate.add(Calendar.DAY_OF_MONTH, 1);
|
||||
|
||||
windDirection.add(new Range(0, 0));
|
||||
windSpeed.add(new Range(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Range {
|
||||
public String from;
|
||||
public String to;
|
||||
|
||||
public Range(final String from, final String to) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
public Range(final int from, final int to) {
|
||||
this.from = String.valueOf(from);
|
||||
this.to = String.valueOf(to);
|
||||
}
|
||||
}
|
||||
|
||||
// /weather/index
|
||||
//
|
||||
// locale=zh_CN
|
||||
// deviceSource=11
|
||||
// days=3
|
||||
// isGlobal=true
|
||||
// locationKey=00.000,-0.000,xiaomi_accu:000000
|
||||
public static class IndexResponse extends Response {
|
||||
public Date pubTime;
|
||||
public List<IndexEntry> dataList = new ArrayList<>();
|
||||
|
||||
public IndexResponse(final WeatherSpec weatherSpec) {
|
||||
pubTime = new Date(weatherSpec.timestamp * 1000L);
|
||||
}
|
||||
}
|
||||
|
||||
private static class IndexEntry {
|
||||
public LocalDate date;
|
||||
public String osi;
|
||||
public String uvi;
|
||||
public Object pai;
|
||||
public String cwi;
|
||||
public String fi;
|
||||
}
|
||||
|
||||
// /weather/current
|
||||
//
|
||||
// locale=zh_CN
|
||||
// deviceSource=11
|
||||
// isGlobal=true
|
||||
// locationKey=00.000,-0.000,xiaomi_accu:000000
|
||||
public static class CurrentResponse extends Response {
|
||||
public CurrentWeatherModel currentWeatherModel;
|
||||
|
||||
public CurrentResponse(final WeatherSpec weatherSpec) {
|
||||
this.currentWeatherModel = new CurrentWeatherModel(weatherSpec);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CurrentWeatherModel {
|
||||
public UnitValue humidity;
|
||||
public UnitValue pressure;
|
||||
public Date pubTime;
|
||||
public UnitValue temperature;
|
||||
public String uvIndex;
|
||||
public UnitValue visibility;
|
||||
public String weather;
|
||||
public Wind wind;
|
||||
|
||||
public CurrentWeatherModel(final WeatherSpec weatherSpec) {
|
||||
humidity = new UnitValue(Unit.PERCENTAGE, weatherSpec.currentHumidity);
|
||||
pressure = new UnitValue(Unit.PRESSURE_MB, "1015"); // ?
|
||||
pubTime = new Date(weatherSpec.timestamp * 1000L);
|
||||
temperature = new UnitValue(Unit.TEMPERATURE_C, weatherSpec.currentTemp - 273);
|
||||
uvIndex = "0";
|
||||
visibility = new UnitValue(Unit.KM, "");
|
||||
weather = String.valueOf(HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode) & 0xff); // is it?
|
||||
wind = new Wind(weatherSpec.windDirection, Math.round(weatherSpec.windSpeed));
|
||||
}
|
||||
}
|
||||
|
||||
private enum Unit {
|
||||
PRESSURE_MB("mb"),
|
||||
PERCENTAGE("%"),
|
||||
TEMPERATURE_C("℃"), // e2 84 83 in UTF-8
|
||||
WIND_DEGREES("°"), // c2 b0 in UTF-8
|
||||
KM("km"),
|
||||
KPH("km/h"),
|
||||
;
|
||||
|
||||
private final String value;
|
||||
|
||||
Unit(final String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnitValue {
|
||||
public String unit;
|
||||
public String value;
|
||||
|
||||
public UnitValue(final Unit unit, final String value) {
|
||||
this.unit = unit.getValue();
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public UnitValue(final Unit unit, final int value) {
|
||||
this.unit = unit.getValue();
|
||||
this.value = String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Wind {
|
||||
public UnitValue direction;
|
||||
public UnitValue speed;
|
||||
|
||||
public Wind(final int direction, final int speed) {
|
||||
this.direction = new UnitValue(Unit.WIND_DEGREES, direction);
|
||||
this.speed = new UnitValue(Unit.KPH, Math.round(speed));
|
||||
}
|
||||
}
|
||||
|
||||
// /weather/forecast/hourly
|
||||
//
|
||||
// locale=zh_CN
|
||||
// deviceSource=11
|
||||
// hourly=72
|
||||
// isGlobal=true
|
||||
// locationKey=00.000,-0.000,xiaomi_accu:000000
|
||||
public static class HourlyResponse extends Response {
|
||||
public Object pubTime;
|
||||
public Object weather;
|
||||
public Object temperature;
|
||||
public Object humidity;
|
||||
public Object fxTime;
|
||||
public Object windDirection;
|
||||
public Object windSpeed;
|
||||
public Object windScale;
|
||||
}
|
||||
|
||||
// /weather/alerts
|
||||
//
|
||||
// locale=zh_CN
|
||||
// deviceSource=11
|
||||
// days=3
|
||||
// isGlobal=true
|
||||
// locationKey=00.000,-0.000,xiaomi_accu:000000
|
||||
public static class AlertsResponse extends Response {
|
||||
}
|
||||
|
||||
private static class LocalDateSerializer implements JsonSerializer<LocalDate> {
|
||||
@Override
|
||||
public JsonElement serialize(final LocalDate src, final Type typeOfSrc, final JsonSerializationContext context) {
|
||||
// Serialize as "yyyy-MM-dd" string
|
||||
return new JsonPrimitive(src.format(DateTimeFormatter.ISO_LOCAL_DATE));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
/**
|
||||
* The workout types, used to start / when workout tracking starts on the band.
|
||||
*/
|
||||
public enum Huami2021WorkoutTrackActivityType {
|
||||
// TODO 150 workouts :/
|
||||
Badminton(0x5c),
|
||||
Dance(0x4c),
|
||||
Elliptical(0x09),
|
||||
Freestyle(0x05),
|
||||
IndoorCycling(0x08),
|
||||
IndoorFitness(0x18),
|
||||
JumpRope(0x15),
|
||||
OutdoorCycling(0x04),
|
||||
OutdoorRunning(0x01),
|
||||
PoolSwimming(0x06),
|
||||
Rowing(0x17),
|
||||
Soccer(0xbf),
|
||||
Treadmill(0x02),
|
||||
Walking(0x03),
|
||||
Yoga(0x3c),
|
||||
;
|
||||
|
||||
private final byte code;
|
||||
|
||||
Huami2021WorkoutTrackActivityType(final int code) {
|
||||
this.code = (byte) code;
|
||||
}
|
||||
|
||||
public byte getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static Huami2021WorkoutTrackActivityType fromCode(final byte code) {
|
||||
for (final Huami2021WorkoutTrackActivityType type : values()) {
|
||||
if (type.getCode() == code) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
@ -108,4 +109,13 @@ public class HuamiBatteryInfo extends AbstractInfo {
|
||||
// }
|
||||
return -1;
|
||||
}
|
||||
|
||||
public GBDeviceEventBatteryInfo toDeviceEvent() {
|
||||
final GBDeviceEventBatteryInfo deviceEventBatteryInfo = new GBDeviceEventBatteryInfo();
|
||||
deviceEventBatteryInfo.level = ((short) getLevelInPercent());
|
||||
deviceEventBatteryInfo.state = getState();
|
||||
deviceEventBatteryInfo.lastChargeTime = getLastChargeTime();
|
||||
deviceEventBatteryInfo.numCharges = getNumCharges();
|
||||
return deviceEventBatteryInfo;
|
||||
}
|
||||
}
|
||||
|
@ -1,147 +0,0 @@
|
||||
/* Copyright (C) 2022 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class HuamiChunked2021Decoder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuamiChunked2021Decoder.class);
|
||||
private Byte currentHandle;
|
||||
private int currentType;
|
||||
private int currentLength;
|
||||
ByteBuffer reassemblyBuffer;
|
||||
private final HuamiSupport huamiSupport;
|
||||
|
||||
public HuamiChunked2021Decoder(HuamiSupport huamiSupport) {
|
||||
this.huamiSupport = huamiSupport;
|
||||
}
|
||||
|
||||
|
||||
public byte[] decode(byte[] data) {
|
||||
int i = 0;
|
||||
if (data[i++] != 0x03) {
|
||||
return null;
|
||||
}
|
||||
boolean encrypted = false;
|
||||
byte flags = data[i++];
|
||||
if ((flags & 0x08) == 0x08) {
|
||||
encrypted = true;
|
||||
}
|
||||
if (huamiSupport.force2021Protocol) {
|
||||
i++; // skip extended header
|
||||
}
|
||||
byte handle = data[i++];
|
||||
if (currentHandle != null && currentHandle != handle) {
|
||||
LOG.warn("ignoring handle " + handle + ", expected " + currentHandle);
|
||||
return null;
|
||||
}
|
||||
byte count = data[i++];
|
||||
if ((flags & 0x01) == 0x01) { // beginning
|
||||
int full_length = (data[i++] & 0xff) | ((data[i++] & 0xff) << 8) | ((data[i++] & 0xff) << 16) | ((data[i++] & 0xff) << 24);
|
||||
currentLength = full_length;
|
||||
if (encrypted) {
|
||||
int encrypted_length = full_length + 8;
|
||||
int overflow = encrypted_length % 16;
|
||||
if (overflow > 0) {
|
||||
encrypted_length += (16 - overflow);
|
||||
}
|
||||
full_length = encrypted_length;
|
||||
}
|
||||
reassemblyBuffer = ByteBuffer.allocate(full_length);
|
||||
currentType = (data[i++] & 0xff) | ((data[i++] & 0xff) << 8);
|
||||
currentHandle = handle;
|
||||
}
|
||||
reassemblyBuffer.put(data, i, data.length - i);
|
||||
if ((flags & 0x02) == 0x02) { // end
|
||||
byte[] buf = reassemblyBuffer.array();
|
||||
if (encrypted) {
|
||||
byte[] messagekey = new byte[16];
|
||||
for (int j = 0; j < 16; j++) {
|
||||
messagekey[j] = (byte) (huamiSupport.sharedSessionKey[j] ^ handle);
|
||||
}
|
||||
try {
|
||||
buf = CryptoUtils.decryptAES(buf, messagekey);
|
||||
buf = ArrayUtils.subarray(buf, 0, currentLength);
|
||||
LOG.info("decrypted data: " + GB.hexdump(buf));
|
||||
} catch (Exception e) {
|
||||
LOG.warn("error decrypting " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (currentType == HuamiService.CHUNKED2021_ENDPOINT_COMPAT) {
|
||||
LOG.info("got configuration data");
|
||||
currentHandle = null;
|
||||
currentType = 0;
|
||||
return ArrayUtils.remove(buf, 0);
|
||||
}
|
||||
if (currentType == HuamiService.CHUNKED2021_ENDPOINT_SMSREPLY && false) { // unsafe for now, disabled, also we shoud return somehing and then parse in HuamiSupport instead of firing stuff here
|
||||
LOG.debug("got command for SMS reply");
|
||||
if (buf[0] == 0x0d) {
|
||||
try {
|
||||
TransactionBuilder builder = huamiSupport.performInitialized("allow sms reply");
|
||||
huamiSupport.writeToChunked2021(builder, (short) 0x0013, huamiSupport.getNextHandle(), new byte[]{(byte) 0x0e, 0x01}, huamiSupport.force2021Protocol, false);
|
||||
builder.queue(huamiSupport.getQueue());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to allow sms reply");
|
||||
}
|
||||
} else if (buf[0] == 0x0b) {
|
||||
String phoneNumber = null;
|
||||
String smsReply = null;
|
||||
for (i = 1; i < buf.length; i++) {
|
||||
if (buf[i] == 0) {
|
||||
phoneNumber = new String(buf, 1, i - 1);
|
||||
// there are four unknown bytes between caller and reply
|
||||
smsReply = new String(buf, i + 5, buf.length - i - 6);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
LOG.debug("will send message '" + smsReply + "' to number '" + phoneNumber + "'");
|
||||
GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl();
|
||||
devEvtNotificationControl.handle = -1;
|
||||
devEvtNotificationControl.phoneNumber = phoneNumber;
|
||||
devEvtNotificationControl.reply = smsReply;
|
||||
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
|
||||
huamiSupport.evaluateGBDeviceEvent(devEvtNotificationControl);
|
||||
try {
|
||||
TransactionBuilder builder = huamiSupport.performInitialized("ack sms reply");
|
||||
byte[] ackSentCommand = new byte[]{0x0c, 0x01};
|
||||
huamiSupport.writeToChunked2021(builder, (short) 0x0013, huamiSupport.getNextHandle(), ackSentCommand, huamiSupport.force2021Protocol, false);
|
||||
builder.queue(huamiSupport.getQueue());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to ack sms reply");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currentHandle = null;
|
||||
currentType = 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
|
||||
|
||||
public abstract class HuamiFirmwareInfo {
|
||||
public abstract class HuamiFirmwareInfo extends AbstractHuamiFirmwareInfo {
|
||||
|
||||
protected static final byte[] RES_HEADER = new byte[]{ // HMRES resources file (*.res)
|
||||
0x48, 0x4d, 0x52, 0x45, 0x53
|
||||
@ -104,9 +104,9 @@ public abstract class HuamiFirmwareInfo {
|
||||
protected static final int COMPRESSED_RES_HEADER_OFFSET = 0x9;
|
||||
protected static final int COMPRESSED_RES_HEADER_OFFSET_NEW = 0xd;
|
||||
|
||||
private HuamiFirmwareType firmwareType;
|
||||
|
||||
@Override
|
||||
public String toVersion(int crc16) {
|
||||
final byte[] bytes = getBytes();
|
||||
String version = getCrcMap().get(crc16);
|
||||
if (version == null) {
|
||||
switch (firmwareType) {
|
||||
@ -165,61 +165,10 @@ public abstract class HuamiFirmwareInfo {
|
||||
return version;
|
||||
}
|
||||
|
||||
public int[] getWhitelistedVersions() {
|
||||
return ArrayUtils.toIntArray(getCrcMap().keySet());
|
||||
}
|
||||
|
||||
private final int crc16;
|
||||
private final int crc32;
|
||||
|
||||
private byte[] bytes;
|
||||
|
||||
public HuamiFirmwareInfo(byte[] bytes) {
|
||||
this.bytes = bytes;
|
||||
crc16 = CheckSums.getCRC16(bytes);
|
||||
crc32 = CheckSums.getCRC32(bytes);
|
||||
firmwareType = determineFirmwareType(bytes);
|
||||
super(bytes);
|
||||
}
|
||||
|
||||
public abstract boolean isGenerallyCompatibleWith(GBDevice device);
|
||||
|
||||
public boolean isHeaderValid() {
|
||||
return getFirmwareType() != HuamiFirmwareType.INVALID;
|
||||
}
|
||||
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of the firmware in number of bytes.
|
||||
*/
|
||||
public int getSize() {
|
||||
return bytes.length;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public int getCrc16() {
|
||||
return crc16;
|
||||
}
|
||||
public int getCrc32() {
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public int getFirmwareVersion() {
|
||||
return getCrc16(); // HACK until we know how to determine the version from the fw bytes
|
||||
}
|
||||
|
||||
public HuamiFirmwareType getFirmwareType() {
|
||||
return firmwareType;
|
||||
}
|
||||
|
||||
protected abstract Map<Integer, String> getCrcMap();
|
||||
|
||||
protected abstract HuamiFirmwareType determineFirmwareType(byte[] bytes);
|
||||
|
||||
protected String searchFirmwareVersion(byte[] fwbytes) {
|
||||
ByteBuffer buf = ByteBuffer.wrap(fwbytes);
|
||||
buf.order(ByteOrder.BIG_ENDIAN);
|
||||
@ -242,26 +191,4 @@ public abstract class HuamiFirmwareInfo {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean searchString32BitAligned(byte[] fwbytes, String findString) {
|
||||
ByteBuffer stringBuf = ByteBuffer.wrap((findString + "\0").getBytes());
|
||||
stringBuf.order(ByteOrder.BIG_ENDIAN);
|
||||
int[] findArray = new int[stringBuf.remaining() / 4];
|
||||
for (int i = 0; i < findArray.length; i++) {
|
||||
findArray[i] = stringBuf.getInt();
|
||||
}
|
||||
|
||||
ByteBuffer buf = ByteBuffer.wrap(fwbytes);
|
||||
buf.order(ByteOrder.BIG_ENDIAN);
|
||||
while (buf.remaining() > 3) {
|
||||
int arrayPos = 0;
|
||||
while (arrayPos < findArray.length && buf.remaining() > 3 && (buf.getInt() == findArray[arrayPos])) {
|
||||
arrayPos++;
|
||||
}
|
||||
if (arrayPos == findArray.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
public enum HuamiFirmwareType {
|
||||
FIRMWARE((byte) 0),
|
||||
CHANGELOG_TXT((byte) 16),
|
||||
// MB7 firmwares are sent as UIHH packing FIRMWARE (zip) + CHANGELOG_TXT, type 0xfd
|
||||
FIRMWARE_UIHH_2021_ZIP_WITH_CHANGELOG((byte) -3),
|
||||
FONT((byte) 1),
|
||||
RES((byte) 2),
|
||||
RES_COMPRESSED((byte) 130),
|
||||
|
@ -16,11 +16,12 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HuamiLanguageType {
|
||||
public static final Map<String, Integer> idLookup = new HashMap<String, Integer>() {{
|
||||
// Use a LinkedHashMap (sorted), so that when we reverse it we get the first value as key, deterministically
|
||||
public static final Map<String, Integer> idLookup = new LinkedHashMap<String, Integer>() {{
|
||||
put("zh_CN", 0x00);
|
||||
put("zh_TW", 0x01);
|
||||
put("zh_HK", 0x01);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,231 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
|
||||
public class UIHHContainer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UIHHContainer.class);
|
||||
|
||||
public static final byte[] UIHH_HEADER = new byte[]{
|
||||
'U', 'I', 'H', 'H'
|
||||
};
|
||||
|
||||
public List<FileEntry> files = new ArrayList<>();
|
||||
|
||||
public List<FileEntry> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public byte[] getFile(final FileType fileType) {
|
||||
for (final FileEntry file : files) {
|
||||
if (file.getType() == fileType) {
|
||||
return file.getContent();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addFile(final FileType type, final byte[] bytes) {
|
||||
files.add(new FileEntry(type, bytes));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] toRawBytes() {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
for (final FileEntry file : files) {
|
||||
try {
|
||||
baos.write(file.buildHeader());
|
||||
baos.write(file.getContent());
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to generate UIHH bytes", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final byte[] contentBytes = baos.toByteArray();
|
||||
final byte[] headerBytes = buildHeader(contentBytes);
|
||||
|
||||
return ArrayUtils.addAll(headerBytes, contentBytes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static UIHHContainer fromRawBytes(final byte[] bytes) {
|
||||
if (bytes.length < 32) {
|
||||
LOG.error("bytes array too small {}", bytes.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.startsWith(bytes, UIHH_HEADER)) {
|
||||
LOG.error("UIHH header not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
final int crc32 = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, 12, 12 + 4));
|
||||
final int length = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, 22, 22 + 4));
|
||||
|
||||
if (length + 32 != bytes.length) {
|
||||
LOG.error("Length mismatch between header and bytes: {}/{}", length, bytes.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (crc32 != CheckSums.getCRC32(ArrayUtils.subarray(bytes, 32, bytes.length))) {
|
||||
LOG.error("CRC mismatch for content");
|
||||
return null;
|
||||
}
|
||||
|
||||
int i = 32;
|
||||
|
||||
final UIHHContainer ret = new UIHHContainer();
|
||||
|
||||
while (i < bytes.length) {
|
||||
if (i + 10 >= bytes.length) {
|
||||
LOG.error("Not enough bytes remaining");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bytes[i] != 1) {
|
||||
LOG.error("Expected 1 at position {}", i);
|
||||
return null;
|
||||
}
|
||||
i++;
|
||||
|
||||
final FileType type = FileType.fromValue(bytes[i]);
|
||||
if (type == null) {
|
||||
LOG.error("Unknown type byte {} at position {}", String.format("0x%x", bytes[i], i));
|
||||
return null;
|
||||
}
|
||||
i++;
|
||||
|
||||
final int fileLength = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, i, i + 4));
|
||||
i += 4;
|
||||
final int fileCrc32 = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, i, i + 4));
|
||||
i += 4;
|
||||
|
||||
if (i + fileLength > bytes.length) {
|
||||
LOG.error("Not enough bytes remaining to read a {} byte file", fileLength);
|
||||
return null;
|
||||
}
|
||||
|
||||
final byte[] fileContent = ArrayUtils.subarray(bytes, i, i + fileLength);
|
||||
if (fileCrc32 != CheckSums.getCRC32(fileContent)) {
|
||||
LOG.error("CRC mismatch for {}", type);
|
||||
return null;
|
||||
}
|
||||
i += fileLength;
|
||||
|
||||
ret.getFiles().add(new FileEntry(type, fileContent));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static byte[] buildHeader(final byte[] content) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(32);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.put(UIHH_HEADER);
|
||||
buf.put(new byte[]{0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01});
|
||||
buf.putInt(CheckSums.getCRC32(content));
|
||||
buf.put(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
buf.putInt(content.length);
|
||||
buf.put(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
|
||||
return buf.array();
|
||||
}
|
||||
|
||||
public enum FileType {
|
||||
FIRMWARE_ZIP(HuamiFirmwareType.FIRMWARE.getValue(), "firmware.zip"),
|
||||
FIRMWARE_CHANGELOG(0x10, "changelog.txt"),
|
||||
GPS_ALM_BIN(HuamiFirmwareType.GPS_ALMANAC.getValue(), "gps_alm.bin"),
|
||||
GLN_ALM_BIN(0x0f, "gln_alm.bin"),
|
||||
LLE_BDS_LLE(0x86, "lle_bds.lle"),
|
||||
LLE_GPS_LLE(0x87, "lle_gps.lle"),
|
||||
LLE_GLO_LLE(0x88, "lle_glo.lle"),
|
||||
LLE_GAL_LLE(0x89, "lle_gal.lle"),
|
||||
LLE_QZSS_LLE(0x8a, "lle_qzss.lle"),
|
||||
;
|
||||
|
||||
private final byte value;
|
||||
private final String name;
|
||||
|
||||
FileType(final int value, final String name) {
|
||||
this.value = (byte) value;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static FileType fromValue(final byte value) {
|
||||
for (final FileType fileType : values()) {
|
||||
if (fileType.getValue() == value) {
|
||||
return fileType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileEntry {
|
||||
private final FileType type;
|
||||
private final byte[] content;
|
||||
|
||||
public FileEntry(final FileType type, final byte[] content) {
|
||||
this.type = type;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public FileType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public byte[] getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public byte[] buildHeader() {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(10);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.put((byte) 0x01);
|
||||
buf.put(type.getValue());
|
||||
buf.putInt(content.length);
|
||||
buf.putInt(CheckSums.getCRC32(content));
|
||||
|
||||
return buf.array();
|
||||
}
|
||||
}
|
||||
}
|
@ -66,63 +66,6 @@ public class MiBand3Support extends AmazfitBipSupport {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
TransactionBuilder builder;
|
||||
try {
|
||||
builder = performInitialized("Sending configuration for option: " + config);
|
||||
switch (config) {
|
||||
case MiBandConst.PREF_NIGHT_MODE:
|
||||
case MiBandConst.PREF_NIGHT_MODE_START:
|
||||
case MiBandConst.PREF_NIGHT_MODE_END:
|
||||
setNightMode(builder);
|
||||
break;
|
||||
default:
|
||||
super.onSendConfiguration(config);
|
||||
return;
|
||||
}
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private MiBand3Support setNightMode(TransactionBuilder builder) {
|
||||
String nightMode = MiBand3Coordinator.getNightMode(gbDevice.getAddress());
|
||||
LOG.info("Setting night mode to " + nightMode);
|
||||
|
||||
switch (nightMode) {
|
||||
case MiBandConst.PREF_NIGHT_MODE_SUNSET:
|
||||
writeToConfiguration(builder, MiBand3Service.COMMAND_NIGHT_MODE_SUNSET);
|
||||
break;
|
||||
case MiBandConst.PREF_NIGHT_MODE_OFF:
|
||||
writeToConfiguration(builder, MiBand3Service.COMMAND_NIGHT_MODE_OFF);
|
||||
break;
|
||||
case MiBandConst.PREF_NIGHT_MODE_SCHEDULED:
|
||||
byte[] cmd = MiBand3Service.COMMAND_NIGHT_MODE_SCHEDULED.clone();
|
||||
|
||||
Calendar calendar = GregorianCalendar.getInstance();
|
||||
|
||||
Date start = MiBand3Coordinator.getNightModeStart(gbDevice.getAddress());
|
||||
calendar.setTime(start);
|
||||
cmd[2] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
|
||||
cmd[3] = (byte) calendar.get(Calendar.MINUTE);
|
||||
|
||||
Date end = MiBand3Coordinator.getNightModeEnd(gbDevice.getAddress());
|
||||
calendar.setTime(end);
|
||||
cmd[4] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
|
||||
cmd[5] = (byte) calendar.get(Calendar.MINUTE);
|
||||
|
||||
writeToConfiguration(builder, cmd);
|
||||
break;
|
||||
default:
|
||||
LOG.error("Invalid night mode: " + nightMode);
|
||||
break;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void phase2Initialize(TransactionBuilder builder) {
|
||||
super.phase2Initialize(builder);
|
||||
|
@ -0,0 +1,55 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband7;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021FirmwareInfo;
|
||||
|
||||
public class MiBand7FirmwareInfo extends Huami2021FirmwareInfo {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBand7FirmwareInfo.class);
|
||||
|
||||
private static final Map<Integer, String> crcToVersion = new HashMap<Integer, String>() {{
|
||||
// firmware
|
||||
}};
|
||||
|
||||
public MiBand7FirmwareInfo(final byte[] bytes) {
|
||||
super(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deviceName() {
|
||||
return HuamiConst.XIAOMI_SMART_BAND7_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerallyCompatibleWith(final GBDevice device) {
|
||||
return isHeaderValid() && device.getType() == DeviceType.MIBAND7;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<Integer, String> getCrcMap() {
|
||||
return crcToVersion;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband7;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband7.MiBand7FWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
|
||||
public class MiBand7Support extends Huami2021Support {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBand7Support.class);
|
||||
|
||||
@Override
|
||||
public HuamiFWHelper createFWHelper(Uri uri, Context context) throws IOException {
|
||||
return new MiBand7FWHelper(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getAllDisplayItems() {
|
||||
return R.array.pref_miband7_display_items_values;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDefaultDisplayItems() {
|
||||
return R.array.pref_miband7_display_items_default;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getAllShortcutItems() {
|
||||
return R.array.pref_miband7_shortcuts_values;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDefaultShortcutItems() {
|
||||
return R.array.pref_miband7_shortcuts_default;
|
||||
}
|
||||
}
|
@ -85,7 +85,6 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
||||
lastPacketCounter = -1;
|
||||
|
||||
TransactionBuilder builder = performInitialized(getName());
|
||||
getSupport().setLowLatency(builder);
|
||||
if (fetchCount == 0) {
|
||||
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
|
||||
}
|
||||
@ -176,6 +175,7 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
||||
}
|
||||
|
||||
private void handleActivityMetadata(byte[] value) {
|
||||
// TODO it's 16 on the MB7
|
||||
if (value.length == 15) {
|
||||
// first two bytes are whether our request was accepted
|
||||
if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
|
||||
|
@ -35,7 +35,7 @@ import androidx.annotation.NonNull;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
@ -44,7 +44,7 @@ public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
|
||||
|
||||
private FileOutputStream logOutputStream;
|
||||
|
||||
public HuamiFetchDebugLogsOperation(AmazfitBipSupport support) {
|
||||
public HuamiFetchDebugLogsOperation(HuamiSupport support) {
|
||||
super(support);
|
||||
setName("fetch debug logs");
|
||||
}
|
||||
|
@ -25,15 +25,23 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Handler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ChunkedDecoder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ChunkedEncoder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class InitOperation2021 extends InitOperation {
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.SUCCESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.RESPONSE;
|
||||
|
||||
public class InitOperation2021 extends InitOperation implements Huami2021Handler {
|
||||
private byte[] privateEC = new byte[24];
|
||||
private byte[] publicEC;
|
||||
private byte[] remotePublicEC = new byte[48];
|
||||
@ -41,10 +49,8 @@ public class InitOperation2021 extends InitOperation {
|
||||
private byte[] sharedEC;
|
||||
private final byte[] finalSharedSessionAES = new byte[16];
|
||||
|
||||
private final byte[] reassembleBuffer = new byte[512];
|
||||
private int lastSequenceNumber = 0;
|
||||
private int reassembleBuffer_pointer = 0;
|
||||
private int reassembleBuffer_expectedBytes = 0;
|
||||
private final Huami2021ChunkedEncoder huami2021ChunkedEncoder;
|
||||
private final Huami2021ChunkedDecoder huami2021ChunkedDecoder;
|
||||
|
||||
static {
|
||||
System.loadLibrary("tiny-edhc");
|
||||
@ -53,8 +59,17 @@ public class InitOperation2021 extends InitOperation {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InitOperation2021.class);
|
||||
|
||||
public InitOperation2021(boolean needsAuth, byte authFlags, byte cryptFlags, HuamiSupport support, TransactionBuilder builder) {
|
||||
public InitOperation2021(final boolean needsAuth,
|
||||
final byte authFlags,
|
||||
final byte cryptFlags,
|
||||
final HuamiSupport support,
|
||||
final TransactionBuilder builder,
|
||||
final Huami2021ChunkedEncoder huami2021ChunkedEncoder,
|
||||
final Huami2021ChunkedDecoder huami2021ChunkedDecoder) {
|
||||
super(needsAuth, authFlags, cryptFlags, support, builder);
|
||||
this.huami2021ChunkedEncoder = huami2021ChunkedEncoder;
|
||||
this.huami2021ChunkedDecoder = huami2021ChunkedDecoder;
|
||||
this.huami2021ChunkedDecoder.setHuami2021Handler(this);
|
||||
}
|
||||
|
||||
private void testAuth() {
|
||||
@ -87,7 +102,7 @@ public class InitOperation2021 extends InitOperation {
|
||||
sendPubkeyCommand[3] = 0x02;
|
||||
System.arraycopy(publicEC, 0, sendPubkeyCommand, 4, 48);
|
||||
//testAuth();
|
||||
huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), sendPubkeyCommand, true, false);
|
||||
huami2021ChunkedEncoder.write(builder, Huami2021Service.CHUNKED2021_ENDPOINT_AUTH, sendPubkeyCommand, true, false);
|
||||
}
|
||||
|
||||
private native byte[] ecdh_generate_public(byte[] privateEC);
|
||||
@ -110,23 +125,66 @@ public class InitOperation2021 extends InitOperation {
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
if (HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ.equals(characteristicUUID)) {
|
||||
byte[] value = characteristic.getValue();
|
||||
if (value.length > 1 && value[0] == 0x03) {
|
||||
int sequenceNumber = value[4];
|
||||
int headerSize;
|
||||
if (sequenceNumber == 0 && value[9] == (byte) HuamiService.CHUNKED2021_ENDPOINT_AUTH && value[10] == 0x00 && value[11] == 0x10 && value[12] == 0x04 && value[13] == 0x01) {
|
||||
reassembleBuffer_pointer = 0;
|
||||
headerSize = 14;
|
||||
reassembleBuffer_expectedBytes = value[5] - 3;
|
||||
|
||||
} else if (sequenceNumber > 0) {
|
||||
if (sequenceNumber != lastSequenceNumber + 1) {
|
||||
LOG.warn("unexpected sequence number");
|
||||
return false;
|
||||
if (!HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ.equals(characteristicUUID)) {
|
||||
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
}
|
||||
headerSize = 5;
|
||||
} else if (value[9] == (byte) HuamiService.CHUNKED2021_ENDPOINT_AUTH && value[10] == 0x00 && value[11] == 0x10 && value[12] == 0x05 && value[13] == 0x01) {
|
||||
|
||||
byte[] value = characteristic.getValue();
|
||||
if (value.length <= 1 || value[0] != 0x03) {
|
||||
// Not chunked
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
}
|
||||
|
||||
this.huami2021ChunkedDecoder.decode(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle2021Payload(final int type, final byte[] payload) {
|
||||
if (type != Huami2021Service.CHUNKED2021_ENDPOINT_AUTH) {
|
||||
this.huamiSupport.handle2021Payload(type, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload[0] == RESPONSE && payload[1] == 0x04 && payload[2] == SUCCESS) {
|
||||
LOG.debug("Got remote random + public key");
|
||||
// Received remote random (16 bytes) + public key (48 bytes)
|
||||
|
||||
System.arraycopy(payload, 3, remoteRandom, 0, 16);
|
||||
System.arraycopy(payload, 19, remotePublicEC, 0, 48);
|
||||
sharedEC = ecdh_generate_shared(privateEC, remotePublicEC);
|
||||
int encryptedSequenceNumber = (sharedEC[0] & 0xff) | ((sharedEC[1] & 0xff) << 8) | ((sharedEC[2] & 0xff) << 16) | ((sharedEC[3] & 0xff) << 24);
|
||||
|
||||
byte[] secretKey = getSecretKey();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]);
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
LOG.debug("Shared Session Key: {}", GB.hexdump(finalSharedSessionAES));
|
||||
}
|
||||
huami2021ChunkedEncoder.setEncryptionParameters(encryptedSequenceNumber, finalSharedSessionAES);
|
||||
huami2021ChunkedDecoder.setEncryptionParameters(finalSharedSessionAES);
|
||||
|
||||
try {
|
||||
byte[] encryptedRandom1 = CryptoUtils.encryptAES(remoteRandom, secretKey);
|
||||
byte[] encryptedRandom2 = CryptoUtils.encryptAES(remoteRandom, finalSharedSessionAES);
|
||||
if (encryptedRandom1.length == 16 && encryptedRandom2.length == 16) {
|
||||
byte[] command = new byte[33];
|
||||
command[0] = 0x05;
|
||||
System.arraycopy(encryptedRandom1, 0, command, 1, 16);
|
||||
System.arraycopy(encryptedRandom2, 0, command, 17, 16);
|
||||
TransactionBuilder builder = createTransactionBuilder("Sending double encryted random to device");
|
||||
huami2021ChunkedEncoder.write(builder, Huami2021Service.CHUNKED2021_ENDPOINT_AUTH, command, true, false);
|
||||
huamiSupport.performImmediately(builder);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("AES encryption failed", e);
|
||||
}
|
||||
} else if (payload[0] == RESPONSE && payload[1] == 0x05 && payload[2] == SUCCESS) {
|
||||
LOG.debug("Auth Success");
|
||||
|
||||
try {
|
||||
TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2");
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
@ -140,53 +198,10 @@ public class InitOperation2021 extends InitOperation {
|
||||
} catch (Exception e) {
|
||||
LOG.error("failed initializing device", e);
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
} else {
|
||||
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
LOG.info("Unhandled auth payload: {}", GB.hexdump(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
int bytesToCopy = value.length - headerSize;
|
||||
System.arraycopy(value, headerSize, reassembleBuffer, reassembleBuffer_pointer, bytesToCopy);
|
||||
reassembleBuffer_pointer += bytesToCopy;
|
||||
|
||||
lastSequenceNumber = sequenceNumber;
|
||||
if (reassembleBuffer_pointer == reassembleBuffer_expectedBytes) {
|
||||
System.arraycopy(reassembleBuffer, 0, remoteRandom, 0, 16);
|
||||
System.arraycopy(reassembleBuffer, 16, remotePublicEC, 0, 48);
|
||||
sharedEC = ecdh_generate_shared(privateEC, remotePublicEC);
|
||||
huamiSupport.encryptedSequenceNr = ((sharedEC[0] & 0xff) | ((sharedEC[1] & 0xff) << 8) | ((sharedEC[2] & 0xff) << 16) | ((sharedEC[3] & 0xff) << 24));
|
||||
|
||||
byte[] secretKey = getSecretKey();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]);
|
||||
}
|
||||
huamiSupport.sharedSessionKey = finalSharedSessionAES;
|
||||
try {
|
||||
byte[] encryptedRandom1 = CryptoUtils.encryptAES(remoteRandom, secretKey);
|
||||
byte[] encryptedRandom2 = CryptoUtils.encryptAES(remoteRandom, finalSharedSessionAES);
|
||||
if (encryptedRandom1.length == 16 && encryptedRandom2.length == 16) {
|
||||
byte[] command = new byte[33];
|
||||
command[0] = 0x05;
|
||||
System.arraycopy(encryptedRandom1, 0, command, 1, 16);
|
||||
System.arraycopy(encryptedRandom2, 0, command, 17, 16);
|
||||
TransactionBuilder builder = createTransactionBuilder("Sending double encryted random to device");
|
||||
huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), command, true, false);
|
||||
huamiSupport.performImmediately(builder);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("AES encryption failed", e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
huamiSupport.logMessageContent(value);
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
} else {
|
||||
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
|
||||
@ -53,7 +54,7 @@ public class UpdateFirmwareOperation extends AbstractHuamiOperation {
|
||||
final BluetoothGattCharacteristic fwCControlChar;
|
||||
final BluetoothGattCharacteristic fwCDataChar;
|
||||
protected final Prefs prefs = GBApplication.getPrefs();
|
||||
protected HuamiFirmwareInfo firmwareInfo;
|
||||
protected AbstractHuamiFirmwareInfo firmwareInfo;
|
||||
|
||||
public UpdateFirmwareOperation(Uri uri, HuamiSupport support) {
|
||||
super(support);
|
||||
@ -81,7 +82,7 @@ public class UpdateFirmwareOperation extends AbstractHuamiOperation {
|
||||
//the firmware will be sent by the notification listener if the band confirms that the metadata are ok.
|
||||
}
|
||||
|
||||
HuamiFirmwareInfo createFwInfo(Uri uri, Context context) throws IOException {
|
||||
AbstractHuamiFirmwareInfo createFwInfo(Uri uri, Context context) throws IOException {
|
||||
HuamiFWHelper fwHelper = getSupport().createFWHelper(uri, context);
|
||||
return fwHelper.getFirmwareInfo();
|
||||
}
|
||||
@ -229,7 +230,7 @@ public class UpdateFirmwareOperation extends AbstractHuamiOperation {
|
||||
* @return whether the transfer succeeded or not. Only a BT layer exception will cause the transmission to fail.
|
||||
* @see #handleNotificationNotif
|
||||
*/
|
||||
private boolean sendFirmwareData(HuamiFirmwareInfo info) {
|
||||
private boolean sendFirmwareData(AbstractHuamiFirmwareInfo info) {
|
||||
byte[] fwbytes = info.getBytes();
|
||||
int len = fwbytes.length;
|
||||
final int packetLength = getSupport().getMTU() - 3;
|
||||
@ -272,7 +273,7 @@ public class UpdateFirmwareOperation extends AbstractHuamiOperation {
|
||||
}
|
||||
|
||||
|
||||
protected void sendChecksum(HuamiFirmwareInfo firmwareInfo) throws IOException {
|
||||
protected void sendChecksum(AbstractHuamiFirmwareInfo firmwareInfo) throws IOException {
|
||||
TransactionBuilder builder = performInitialized("send firmware checksum");
|
||||
int crc16 = firmwareInfo.getCrc16();
|
||||
byte[] bytes = BLETypeConversions.fromUint16(crc16);
|
||||
@ -284,7 +285,7 @@ public class UpdateFirmwareOperation extends AbstractHuamiOperation {
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
HuamiFirmwareInfo getFirmwareInfo() {
|
||||
AbstractHuamiFirmwareInfo getFirmwareInfo() {
|
||||
return firmwareInfo;
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
@ -47,6 +48,7 @@ public class UpdateFirmwareOperation2020 extends UpdateFirmwareOperation {
|
||||
}
|
||||
|
||||
private final byte COMMAND_REQUEST_PARAMETERS = (byte) 0xd0;
|
||||
private final byte COMMAND_UNKNOWN_D1 = (byte) 0xd1;
|
||||
private final byte COMMAND_SEND_FIRMWARE_INFO = (byte) 0xd2;
|
||||
private final byte COMMAND_START_TRANSFER = (byte) 0xd3;
|
||||
private final byte REPLY_UPDATE_PROGRESS = (byte) 0xd4;
|
||||
@ -71,12 +73,12 @@ public class UpdateFirmwareOperation2020 extends UpdateFirmwareOperation {
|
||||
|
||||
@Override
|
||||
protected void handleNotificationNotif(byte[] value) {
|
||||
if (value.length != 3 && value.length != 6 && value.length != 11) {
|
||||
LOG.error("Notifications should be 3, 6 or 11 bytes long.");
|
||||
if (value.length != 3 && value.length != 6 && value.length != 7 && value.length != 11) {
|
||||
LOG.error("Notifications should be 3, 6, 7 or 11 bytes long.");
|
||||
getSupport().logMessageContent(value);
|
||||
return;
|
||||
}
|
||||
boolean success = (value[2] == HuamiService.SUCCESS) || ((value[1] == REPLY_UPDATE_PROGRESS) && value.length == 6); // ugly
|
||||
boolean success = (value[2] == HuamiService.SUCCESS) || ((value[1] == REPLY_UPDATE_PROGRESS) && value.length >= 6); // ugly
|
||||
|
||||
if (value[0] == HuamiService.RESPONSE && success) {
|
||||
try {
|
||||
@ -214,7 +216,7 @@ public class UpdateFirmwareOperation2020 extends UpdateFirmwareOperation {
|
||||
}
|
||||
|
||||
|
||||
private boolean sendFirmwareDataChunk(HuamiFirmwareInfo info, int offset) {
|
||||
private boolean sendFirmwareDataChunk(AbstractHuamiFirmwareInfo info, int offset) {
|
||||
byte[] fwbytes = info.getBytes();
|
||||
int len = fwbytes.length;
|
||||
int remaining = len - offset;
|
||||
|
@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
@ -87,7 +88,7 @@ public class UpdateFirmwareOperationNew extends UpdateFirmwareOperation {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendChecksum(HuamiFirmwareInfo firmwareInfo) throws IOException {
|
||||
protected void sendChecksum(AbstractHuamiFirmwareInfo firmwareInfo) throws IOException {
|
||||
TransactionBuilder builder = performInitialized("send firmware upload finished");
|
||||
builder.write(fwCControlChar, new byte[]{HuamiService.COMMAND_FIRMWARE_CHECKSUM});
|
||||
builder.queue(getQueue());
|
||||
|
@ -208,6 +208,12 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneFound() {
|
||||
byte[] bytes = gbDeviceProtocol.encodePhoneFound();
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenshotReq() {
|
||||
byte[] bytes = gbDeviceProtocol.encodeScreenshotReq();
|
||||
|
@ -110,6 +110,10 @@ public abstract class GBDeviceProtocol {
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] encodePhoneFound() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] encodeEnableRealtimeSteps(boolean enable) {
|
||||
return null;
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ import android.graphics.RectF;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BitmapUtil {
|
||||
/**
|
||||
* Downscale a bitmap to a maximum resolution. Doesn't scale if the bitmap is already smaller than the max resolution.
|
||||
@ -205,4 +207,81 @@ public class BitmapUtil {
|
||||
canvas.drawBitmap(bmp2, new Matrix(), null);
|
||||
return bmOverlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@link Drawable} to a {@link Bitmap}, in ARGB8888 mode.
|
||||
*
|
||||
* @param drawable the {@link Drawable}
|
||||
* @return the {@link Bitmap}
|
||||
*/
|
||||
public static Bitmap toBitmap(final Drawable drawable) {
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
return ((BitmapDrawable) drawable).getBitmap();
|
||||
}
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@link Bitmap} to an uncompressed TGA image, as raw bytes, in RGB565 encoding.
|
||||
* @param bmp the {@link Bitmap} to convert.
|
||||
* @param width the target width
|
||||
* @param height the target height
|
||||
* @param id the TGA ID
|
||||
* @return the raw bytes for the TGA image
|
||||
*/
|
||||
public static byte[] convertToTgaRGB565(final Bitmap bmp, final int width, final int height, final byte[] id) {
|
||||
final Bitmap bmp565;
|
||||
if (bmp.getConfig().equals(Bitmap.Config.RGB_565) && bmp.getWidth() == width && bmp.getHeight() == height) {
|
||||
// Right encoding and size
|
||||
bmp565 = bmp;
|
||||
} else {
|
||||
// Convert encoding / scale
|
||||
bmp565 = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
|
||||
final Canvas canvas = new Canvas(bmp565);
|
||||
final Rect rect = new Rect(0, 0, width, height);
|
||||
canvas.drawBitmap(bmp, null, rect, null);
|
||||
}
|
||||
|
||||
int size = bmp565.getRowBytes() * bmp565.getHeight();
|
||||
final ByteBuffer bmp565buf = ByteBuffer.allocate(size);
|
||||
bmp565.copyPixelsToBuffer(bmp565buf);
|
||||
|
||||
// As per https://en.wikipedia.org/wiki/Truevision_TGA
|
||||
// 18 bytes
|
||||
final byte[] header = {
|
||||
// ID length
|
||||
(byte) id.length,
|
||||
// Color map type - (0 - no color map)
|
||||
0x00,
|
||||
// Image type (2 - uncompressed true-color image)
|
||||
0x02,
|
||||
// Color map specification (5 bytes)
|
||||
0x00, 0x00, // first entry index
|
||||
0x00, 0x00, /// color map length
|
||||
0x00, // color map entry size
|
||||
// Image dimensions and format (10 bytes)
|
||||
0x00, 0x00, // x origin
|
||||
0x00, 0x00, // y origin
|
||||
(byte) (width & 0xff), (byte) ((width >> 8) & 0xff), // width
|
||||
(byte) (height & 0xff), (byte) ((height >> 8) & 0xff), // height
|
||||
16, // bits per pixel (10)
|
||||
0x20, // image descriptor (0x20, 00100000)
|
||||
// bits 3-0 give the alpha channel depth, bits 5-4 give pixel ordering
|
||||
// Bit 4 of the image descriptor byte indicates right-to-left pixel ordering if set.
|
||||
// Bit 5 indicates an ordering of top-to-bottom. Otherwise, pixels are stored in bottom-to-top, left-to-right order.
|
||||
};
|
||||
|
||||
final ByteBuffer tga565buf = ByteBuffer.allocate(header.length + id.length + size);
|
||||
|
||||
tga565buf.put(header);
|
||||
tga565buf.put(id);
|
||||
tga565buf.put(bmp565buf.array());
|
||||
|
||||
return tga565buf.array();
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitpoppro.AmazfitP
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfittrexpro.AmazfitTRexProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitx.AmazfitXCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband6.MiBand6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband7.MiBand7Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppe.ZeppECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr2.AmazfitGTR2eCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgts.AmazfitGTSCoordinator;
|
||||
@ -283,6 +284,7 @@ public class DeviceHelper {
|
||||
result.add(new MiBand4Coordinator());
|
||||
result.add(new MiBand5Coordinator());
|
||||
result.add(new MiBand6Coordinator());
|
||||
result.add(new MiBand7Coordinator());
|
||||
result.add(new MiBand2HRXCoordinator());
|
||||
result.add(new MiBand2Coordinator()); // Note: MiBand2 and all of the above must come before MiBand because detection is hacky, atm
|
||||
result.add(new MiBandCoordinator());
|
||||
|
@ -105,12 +105,12 @@ public class GBPrefs {
|
||||
}
|
||||
|
||||
public String getTimeFormat() {
|
||||
String timeFormat = mPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, "auto");
|
||||
if ("auto".equals(timeFormat)) {
|
||||
String timeFormat = mPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_AUTO);
|
||||
if (DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_AUTO.equals(timeFormat)) {
|
||||
if (DateFormat.is24HourFormat(GBApplication.getContext())) {
|
||||
timeFormat = "24h";
|
||||
timeFormat = DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H;
|
||||
} else {
|
||||
timeFormat = "am/pm";
|
||||
timeFormat = DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,42 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class MapUtils {
|
||||
/**
|
||||
* Reverses a map. If there are multiple values for the same key, the first one will take precedence.
|
||||
*
|
||||
* @param map the map to reverse
|
||||
* @param <V> the type for the values
|
||||
* @param <K> the type for the keys
|
||||
* @return the reversed map
|
||||
*/
|
||||
public static <V, K> Map<V, K> reverse(final Map<K, V> map) {
|
||||
final Map<V, K> reversed = new HashMap<>();
|
||||
|
||||
for (final Map.Entry<K, V> entry : map.entrySet()) {
|
||||
if (!reversed.containsKey(entry.getValue())) {
|
||||
reversed.put(entry.getValue(), entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
return reversed;
|
||||
}
|
||||
}
|
@ -23,6 +23,9 @@ import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
public class StringUtils {
|
||||
|
||||
@ -133,6 +136,17 @@ public class StringUtils {
|
||||
return new String(newArray);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String untilNullTerminator(final byte[] bytes, final int startOffset) {
|
||||
for (int i = startOffset; i < bytes.length; i++) {
|
||||
if (bytes[i] == 0) {
|
||||
return new String(ArrayUtils.subarray(bytes, startOffset, i));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] array) {
|
||||
return GB.hexdump(array, 0, -1);
|
||||
}
|
||||
|
5
app/src/main/res/drawable/ic_always_on_display.xml
Normal file
5
app/src/main/res/drawable/ic_always_on_display.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#7E7E7E"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,18H7V6h10V18zM8,10h8v1.5H8V10zM9,13h6v1.5H9V13z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_hourglass_empty.xml
Normal file
5
app/src/main/res/drawable/ic_hourglass_empty.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#7E7E7E"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,2v6h0.01L6,8.01 10,12l-4,4 0.01,0.01L6,16.01L6,22h12v-5.99h-0.01L18,16l-4,-4 4,-3.99 -0.01,-0.01L18,8L18,2L6,2zM16,16.5L16,20L8,20v-3.5l4,-4 4,4zM12,11.5l-4,-4L8,4h8v3.5l-4,4z"/>
|
||||
</vector>
|
@ -123,6 +123,14 @@
|
||||
grid:layout_gravity="fill_horizontal"
|
||||
android:text="Set Activity Fetch Time" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/setWeatherButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
grid:layout_columnSpan="2"
|
||||
grid:layout_gravity="fill_horizontal"
|
||||
android:text="Set Weather" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/factoryResetButton"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -232,6 +232,17 @@
|
||||
<item>MM/dd/yyyy</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="dateformats_2">
|
||||
<item>YYYY/MM/DD</item>
|
||||
<item>DD/MM/YYYY</item>
|
||||
<item>MM/DD/YYYY</item>
|
||||
</string-array>
|
||||
<string-array name="dateformats_2_values">
|
||||
<item>YYYY/MM/DD</item>
|
||||
<item>DD/MM/YYYY</item>
|
||||
<item>MM/DD/YYYY</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="do_not_disturb_no_auto">
|
||||
<item>@string/mi2_dnd_off</item>
|
||||
<item>@string/mi2_dnd_scheduled</item>
|
||||
@ -252,6 +263,19 @@
|
||||
<item>@string/p_scheduled</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="do_not_disturb_with_always">
|
||||
<item>@string/mi2_dnd_off</item>
|
||||
<item>@string/mi2_dnd_always</item>
|
||||
<item>@string/mi2_dnd_automatic</item>
|
||||
<item>@string/mi2_dnd_scheduled</item>
|
||||
</string-array>
|
||||
<string-array name="do_not_disturb_with_always_values">
|
||||
<item>@string/p_off</item>
|
||||
<item>@string/p_always</item>
|
||||
<item>@string/p_automatic</item>
|
||||
<item>@string/p_scheduled</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="zetime_do_not_disturb">
|
||||
<item>@string/mi2_dnd_off</item>
|
||||
<item>@string/mi2_dnd_scheduled</item>
|
||||
@ -261,6 +285,19 @@
|
||||
<item>@string/p_scheduled</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="always_on_display">
|
||||
<item>@string/off</item>
|
||||
<item>@string/always</item>
|
||||
<item>@string/mi2_dnd_automatic</item>
|
||||
<item>@string/mi2_dnd_scheduled</item>
|
||||
</string-array>
|
||||
<string-array name="always_on_display_values">
|
||||
<item>@string/p_off</item>
|
||||
<item>@string/p_always</item>
|
||||
<item>@string/p_automatic</item>
|
||||
<item>@string/p_scheduled</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="activate_display_on_lift">
|
||||
<item>@string/off</item>
|
||||
<item>@string/on</item>
|
||||
@ -587,6 +624,136 @@
|
||||
<item>@string/p_menuitem_music</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_miband7_display_items">
|
||||
<item>@string/menuitem_personal_activity_intelligence</item>
|
||||
<item>@string/menuitem_hr</item>
|
||||
<item>@string/menuitem_workout</item>
|
||||
<item>@string/menuitem_weather</item>
|
||||
<item>@string/menuitem_alarm</item>
|
||||
<item>@string/menuitem_music</item>
|
||||
<item>@string/menuitem_stopwatch</item>
|
||||
<item>@string/menuitem_countdown</item>
|
||||
<item>@string/menuitem_findphone</item>
|
||||
<item>@string/menuitem_mutephone</item>
|
||||
<item>@string/menuitem_settings</item>
|
||||
<item>@string/menuitem_more</item>
|
||||
<item>@string/menuitem_workout_history</item>
|
||||
<item>@string/menuitem_eventreminder</item>
|
||||
<item>@string/menuitem_pai</item>
|
||||
<item>@string/menuitem_stress</item>
|
||||
<item>@string/menuitem_female_health</item>
|
||||
<item>@string/menuitem_workout_status</item>
|
||||
<item>@string/menuitem_sleep</item>
|
||||
<item>@string/menuitem_spo2</item>
|
||||
<item>@string/menuitem_events</item>
|
||||
<item>@string/menuitem_breathing</item>
|
||||
<item>@string/menuitem_pomodoro</item>
|
||||
<item>@string/menuitem_flashlight</item>
|
||||
<item>@string/menuitem_takephoto</item>
|
||||
<item>@string/menuitem_worldclock</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_miband7_display_items_values">
|
||||
<item>@string/p_menuitem_personal_activity_intelligence</item>
|
||||
<item>@string/p_menuitem_hr</item>
|
||||
<item>@string/p_menuitem_workout</item>
|
||||
<item>@string/p_menuitem_weather</item>
|
||||
<item>@string/p_menuitem_alarm</item>
|
||||
<item>@string/p_menuitem_music</item>
|
||||
<item>@string/p_menuitem_stopwatch</item>
|
||||
<item>@string/p_menuitem_countdown</item>
|
||||
<item>@string/p_menuitem_findphone</item>
|
||||
<item>@string/p_menuitem_mutephone</item>
|
||||
<item>@string/p_menuitem_settings</item>
|
||||
<item>@string/p_menuitem_more</item>
|
||||
<item>@string/p_menuitem_workout_history</item>
|
||||
<item>@string/p_menuitem_eventreminder</item>
|
||||
<item>@string/p_menuitem_pai</item>
|
||||
<item>@string/p_menuitem_stress</item>
|
||||
<item>@string/p_menuitem_female_health</item>
|
||||
<item>@string/p_menuitem_workout_status</item>
|
||||
<item>@string/p_menuitem_sleep</item>
|
||||
<item>@string/p_menuitem_spo2</item>
|
||||
<item>@string/p_menuitem_events</item>
|
||||
<item>@string/p_menuitem_breathing</item>
|
||||
<item>@string/p_menuitem_pomodoro</item>
|
||||
<item>@string/p_menuitem_flashlight</item>
|
||||
<item>@string/p_menuitem_takephoto</item>
|
||||
<item>@string/p_menuitem_worldclock</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_miband7_display_items_default">
|
||||
<item>@string/p_menuitem_workout</item>
|
||||
<item>@string/p_menuitem_alarm</item>
|
||||
<item>@string/p_menuitem_weather</item>
|
||||
<item>@string/p_menuitem_stopwatch</item>
|
||||
<item>@string/p_menuitem_countdown</item>
|
||||
<item>@string/p_menuitem_events</item>
|
||||
<item>@string/p_menuitem_eventreminder</item>
|
||||
<item>@string/p_menuitem_findphone</item>
|
||||
<item>@string/p_menuitem_mutephone</item>
|
||||
<item>@string/p_menuitem_flashlight</item>
|
||||
<item>@string/p_menuitem_more</item>
|
||||
<item>@string/p_menuitem_hr</item>
|
||||
<item>@string/p_menuitem_spo2</item>
|
||||
<item>@string/p_menuitem_sleep</item>
|
||||
<item>@string/p_menuitem_stress</item>
|
||||
<item>@string/p_menuitem_workout_status</item>
|
||||
<item>@string/p_menuitem_workout_history</item>
|
||||
<item>@string/p_menuitem_pai</item>
|
||||
<item>@string/p_menuitem_personal_activity_intelligence</item>
|
||||
<item>@string/p_menuitem_female_health</item>
|
||||
<item>@string/p_menuitem_breathing</item>
|
||||
<item>@string/p_menuitem_pomodoro</item>
|
||||
<item>@string/p_menuitem_music</item>
|
||||
<item>@string/p_menuitem_worldclock</item>
|
||||
<item>@string/p_menuitem_takephoto</item>
|
||||
<item>@string/p_menuitem_settings</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_miband7_shortcuts">
|
||||
<item>@string/menuitem_hr</item>
|
||||
<item>@string/menuitem_workout</item>
|
||||
<item>@string/menuitem_workout_status</item>
|
||||
<item>@string/menuitem_weather</item>
|
||||
<item>@string/menuitem_alarm</item>
|
||||
<item>@string/menuitem_music</item>
|
||||
<item>@string/menuitem_activity</item>
|
||||
<item>@string/menuitem_eventreminder</item>
|
||||
<item>@string/menuitem_female_health</item>
|
||||
<item>@string/menuitem_pai</item>
|
||||
<item>@string/menuitem_stress</item>
|
||||
<item>@string/menuitem_sleep</item>
|
||||
<item>@string/menuitem_spo2</item>
|
||||
<item>@string/menuitem_events</item>
|
||||
<item>@string/menuitem_breathing</item>
|
||||
<item>@string/menuitem_worldclock</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_miband7_shortcuts_values">
|
||||
<item>@string/p_menuitem_hr</item>
|
||||
<item>@string/p_menuitem_workout</item>
|
||||
<item>@string/p_menuitem_workout_status</item>
|
||||
<item>@string/p_menuitem_weather</item>
|
||||
<item>@string/p_menuitem_alarm</item>
|
||||
<item>@string/p_menuitem_music</item>
|
||||
<item>@string/p_menuitem_activity</item>
|
||||
<item>@string/p_menuitem_eventreminder</item>
|
||||
<item>@string/p_menuitem_female_health</item>
|
||||
<item>@string/p_menuitem_pai</item>
|
||||
<item>@string/p_menuitem_stress</item>
|
||||
<item>@string/p_menuitem_sleep</item>
|
||||
<item>@string/p_menuitem_spo2</item>
|
||||
<item>@string/p_menuitem_events</item>
|
||||
<item>@string/p_menuitem_breathing</item>
|
||||
<item>@string/p_menuitem_worldclock</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_miband7_shortcuts_default">
|
||||
<item>@string/p_menuitem_weather</item>
|
||||
<item>@string/p_menuitem_music</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_miband5_workout_activity_types">
|
||||
<item>@string/activity_type_outdoor_running</item>
|
||||
<item>@string/activity_type_walking</item>
|
||||
@ -1592,11 +1759,27 @@
|
||||
<item>3600</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_heartrate_measurement_interval_with_smart">
|
||||
<item name="0">@string/off</item>
|
||||
<item name="-1">@string/smart</item>
|
||||
<item name="60">@string/interval_one_minute</item>
|
||||
<item name="600">@string/interval_ten_minutes</item>
|
||||
<item name="1800">@string/interval_thirty_minutes</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_heartrate_measurement_interval_with_smart_values">
|
||||
<item>0</item>
|
||||
<item>-1</item>
|
||||
<item>60</item>
|
||||
<item>600</item>
|
||||
<item>1800</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_miband_heartrate_alert_threshold">
|
||||
<item name="100">@string/heartrate_bpm_100</item>
|
||||
<item name="105">@string/heartrate_bpm_105</item>
|
||||
<item name="110">@string/heartrate_bpm_110</item>
|
||||
<item name="112">@string/heartrate_bpm_112</item>
|
||||
<item name="115">@string/heartrate_bpm_115</item>
|
||||
<item name="120">@string/heartrate_bpm_120</item>
|
||||
<item name="125">@string/heartrate_bpm_125</item>
|
||||
<item name="130">@string/heartrate_bpm_130</item>
|
||||
@ -1610,7 +1793,7 @@
|
||||
<item>100</item>
|
||||
<item>105</item>
|
||||
<item>110</item>
|
||||
<item>112</item>
|
||||
<item>115</item>
|
||||
<item>120</item>
|
||||
<item>125</item>
|
||||
<item>130</item>
|
||||
@ -1620,6 +1803,54 @@
|
||||
<item>150</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_miband_heartrate_high_alert_threshold_with_off">
|
||||
<item name="0">@string/off</item>
|
||||
<item name="100">@string/heartrate_bpm_100</item>
|
||||
<item name="110">@string/heartrate_bpm_110</item>
|
||||
<item name="120">@string/heartrate_bpm_120</item>
|
||||
<item name="130">@string/heartrate_bpm_130</item>
|
||||
<item name="140">@string/heartrate_bpm_140</item>
|
||||
<item name="150">@string/heartrate_bpm_150</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_miband_heartrate_high_alert_threshold_with_off_values">
|
||||
<item>0</item>
|
||||
<item>100</item>
|
||||
<item>110</item>
|
||||
<item>120</item>
|
||||
<item>130</item>
|
||||
<item>140</item>
|
||||
<item>150</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_miband_heartrate_low_alert_threshold">
|
||||
<item name="0">@string/off</item>
|
||||
<item name="40">@string/heartrate_bpm_40</item>
|
||||
<item name="45">@string/heartrate_bpm_45</item>
|
||||
<item name="50">@string/heartrate_bpm_50</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_miband_heartrate_low_alert_threshold_values">
|
||||
<item>0</item>
|
||||
<item>40</item>
|
||||
<item>45</item>
|
||||
<item>50</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_spo2_alert_threshold">
|
||||
<item name="80">@string/spo2_perc_80</item>
|
||||
<item name="85">@string/spo2_perc_85</item>
|
||||
<item name="90">@string/spo2_perc_90</item>
|
||||
<item name="off">@string/spo2_off</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_spo2_alert_threshold_values">
|
||||
<item>80</item>
|
||||
<item>85</item>
|
||||
<item>90</item>
|
||||
<item>0</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="prefs_zetime_heartrate_measurement_interval">
|
||||
<item name="0">@string/off</item>
|
||||
<item name="300">@string/interval_five_minutes</item>
|
||||
@ -1663,6 +1894,33 @@
|
||||
<item>1800</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="screen_timeout_5_to_15">
|
||||
<item>@string/seconds_5</item>
|
||||
<item>@string/seconds_6</item>
|
||||
<item>@string/seconds_7</item>
|
||||
<item>@string/seconds_8</item>
|
||||
<item>@string/seconds_9</item>
|
||||
<item>@string/seconds_10</item>
|
||||
<item>@string/seconds_11</item>
|
||||
<item>@string/seconds_12</item>
|
||||
<item>@string/seconds_13</item>
|
||||
<item>@string/seconds_14</item>
|
||||
<item>@string/seconds_15</item>
|
||||
</string-array>
|
||||
<string-array name="screen_timeout_5_to_15_values">
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
<item>10</item>
|
||||
<item>11</item>
|
||||
<item>12</item>
|
||||
<item>13</item>
|
||||
<item>14</item>
|
||||
<item>15</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="reminder_repeat">
|
||||
<item>@string/reminder_once</item>
|
||||
<item>@string/reminder_every_day</item>
|
||||
|
@ -131,6 +131,7 @@
|
||||
<string name="fw_upgrade_notice_miband4">You are about to install the %s firmware on your Mi Band 4.\n\nPlease make sure to install the .fw file, and after that the .res file. Your band will reboot after installing the .fw file.\n\nNote: You do not have to install .res if it is exactly the same as the one previously installed.\n\nPROCEED AT YOUR OWN RISK!</string>
|
||||
<string name="fw_upgrade_notice_miband5">You are about to install the %s firmware on your Mi Band 5.\n\nPlease make sure to install the .fw file, and after that the .res file. Your band will reboot after installing the .fw file.\n\nNote: You do not have to install .res if it is exactly the same as the one previously installed.\n\nPROCEED AT YOUR OWN RISK!</string>
|
||||
<string name="fw_upgrade_notice_miband6">You are about to install the %s firmware on your Mi Band 6.\n\nPlease make sure to install the .fw file, and after that the .res file. Your band will reboot after installing the .fw file.\n\nNote: You do not have to install .res if it is exactly the same as the one previously installed.\n\nPROCEED AT YOUR OWN RISK!</string>
|
||||
<string name="fw_upgrade_notice_miband7">You are about to install the %s firmware on your Xiaomi Smart Band 7.\n\nYour band will reboot after installing the .zip file.\n\nPROCEED AT YOUR OWN RISK!</string>
|
||||
<string name="fw_upgrade_notice_amazfitx">You are about to install the %s firmware on your Amazfit X.\n\nPlease make sure to install the .fw file, and after that the .res file. Your band will reboot after installing the .fw file.\n\nNote: You do not have to install .res if it is exactly the same as the one previously installed.\n\nPROCEED AT YOUR OWN RISK!</string>
|
||||
<string name="fw_upgrade_notice_amazfitneo">You are about to install the %s firmware on your Amazfit Neo.
|
||||
\n
|
||||
@ -158,6 +159,12 @@
|
||||
<string name="pref_header_general">General settings</string>
|
||||
<string name="pref_header_other">Other</string>
|
||||
<string name="pref_header_system">System</string>
|
||||
<string name="pref_header_calendar">Calendar</string>
|
||||
<string name="pref_header_connection">Connection</string>
|
||||
<string name="pref_header_display">Display</string>
|
||||
<string name="pref_header_health">Health</string>
|
||||
<string name="pref_header_time">Time</string>
|
||||
<string name="pref_header_workout">Workout</string>
|
||||
<string name="pref_header_equalizer">Equalizer</string>
|
||||
<string name="pref_title_general_autoconnectonbluetooth">Connect to Gadgetbridge device when Bluetooth is turned on</string>
|
||||
<string name="pref_title_general_autostartonboot">Start automatically</string>
|
||||
@ -228,6 +235,7 @@
|
||||
<string name="pref_blacklist_calendars_summary">Blacklisted calendars will not be synced to the device</string>
|
||||
<string name="pref_header_cannned_messages">Canned messages</string>
|
||||
<string name="pref_title_canned_replies">Replies</string>
|
||||
<string name="pref_summary_canned_replies">Reply from the watch using preset messages</string>
|
||||
<string name="pref_title_canned_reply_suffix">Common suffix</string>
|
||||
<string name="pref_title_canned_messages_dismisscall">Call Dismissal</string>
|
||||
<string name="pref_title_canned_messages_set">Update on device</string>
|
||||
@ -252,6 +260,8 @@
|
||||
<string name="pref_summary_custom_deviceicon">Show a device specific Android notification icon instead the Gadgetbridge icon when connected</string>
|
||||
<string name="pref_title_autoremove_notifications">Autoremove dismissed notifications</string>
|
||||
<string name="pref_summary_autoremove_notifications">Notifications are automatically removed from the device when dismissed from the phone</string>
|
||||
<string name="pref_title_screen_on_on_notifications">Screen On on Notifications</string>
|
||||
<string name="pref_summary_screen_on_on_notifications">Turn on the band\'s screen when a notification arrives</string>
|
||||
<string name="pref_title_pebble_privacy_mode">Privacy mode</string>
|
||||
<string name="pref_pebble_privacy_mode_off">Normal notifications</string>
|
||||
<string name="pref_pebble_privacy_mode_content">Shift the notification text off-screen</string>
|
||||
@ -480,6 +490,8 @@
|
||||
<string name="watch9_pairing_tap_hint">When your watch vibrates, shake the device or press its button.</string>
|
||||
<string name="title_activity_sleepmonitor">Sleep monitor</string>
|
||||
<string name="pref_write_logfiles">Write log files</string>
|
||||
<string name="pref_cache_weather">Cache weather information</string>
|
||||
<string name="pref_cache_weather_summary">Weather information will be cached across application restarts.</string>
|
||||
<string name="pref_write_logfiles_not_available">File logging initialization failed, writing log files is currently not available. Restart the application to attempt to initialize the log files again.</string>
|
||||
<string name="initializing">Initializing</string>
|
||||
<string name="busy_task_fetch_activity_data">Fetching activity data</string>
|
||||
@ -520,10 +532,13 @@
|
||||
<string name="interval_fifteen_minutes">every 15 minutes</string>
|
||||
<string name="interval_thirty_minutes">every 30 minutes</string>
|
||||
<string name="interval_forty_five_minutes">every 45 minutes</string>
|
||||
<string name="heartrate_bpm_40">40 bpm</string>
|
||||
<string name="heartrate_bpm_45">45 bpm</string>
|
||||
<string name="heartrate_bpm_50">50 bpm</string>
|
||||
<string name="heartrate_bpm_100">100 bpm</string>
|
||||
<string name="heartrate_bpm_105">105 bpm</string>
|
||||
<string name="heartrate_bpm_110">110 bpm</string>
|
||||
<string name="heartrate_bpm_112">112 bpm</string>
|
||||
<string name="heartrate_bpm_115">115 bpm</string>
|
||||
<string name="heartrate_bpm_120">120 bpm</string>
|
||||
<string name="heartrate_bpm_125">125 bpm</string>
|
||||
<string name="heartrate_bpm_130">130 bpm</string>
|
||||
@ -531,6 +546,10 @@
|
||||
<string name="heartrate_bpm_140">140 bpm</string>
|
||||
<string name="heartrate_bpm_145">145 bpm</string>
|
||||
<string name="heartrate_bpm_150">150 bpm</string>
|
||||
<string name="spo2_perc_80">80%</string>
|
||||
<string name="spo2_perc_85">85%</string>
|
||||
<string name="spo2_perc_90">90%</string>
|
||||
<string name="spo2_off">Off</string>
|
||||
<string name="interval_one_hour">once an hour</string>
|
||||
<string name="stats_title">Speed zones</string>
|
||||
<string name="stats_x_axis_label">Total minutes</string>
|
||||
@ -590,8 +609,15 @@
|
||||
<string name="prefs_heartrate_alert_experimental_title">Heart rate alert (experimental)</string>
|
||||
<string name="prefs_heartrate_alert_experimental_description">Vibrate the band when the heart rate is over a threshold, without any obvious physical activity in the last 10 minutes. This feature is experimental, and was not extensively tested.</string>
|
||||
<string name="prefs_heartrate_alert_threshold">Heart rate alert threshold</string>
|
||||
<string name="prefs_heartrate_alert_high_threshold">High heart rate alert threshold</string>
|
||||
<string name="prefs_heartrate_alert_low_threshold">Low heart rate alert threshold</string>
|
||||
<string name="prefs_stress_monitoring_title">Stress monitoring</string>
|
||||
<string name="prefs_stress_monitoring_description">Monitor stress level while resting</string>
|
||||
<string name="prefs_relaxation_reminder_title">Relaxation reminder</string>
|
||||
<string name="prefs_relaxation_reminder_description">Vibrate the band to notify you if the stress value is higher than 80</string>
|
||||
<string name="prefs_spo2_monitoring_title">Blood Oxygen Monitoring</string>
|
||||
<string name="prefs_spo2_monitoring_description">Automatically monitor the blood oxygen levels throughout the day</string>
|
||||
<string name="prefs_spo2_alert_threshold">SPO2 alert threshold</string>
|
||||
<string name="prefs_activity_monitoring_title">Activity monitoring</string>
|
||||
<string name="prefs_activity_monitoring_description">Automatically increase the heart rate detection frequency when the band detects physical exercise, to increase heart rate capture accuracy.</string>
|
||||
<string name="dbaccess_error_executing">Error executing \'%1$s\'</string>
|
||||
@ -666,7 +692,16 @@
|
||||
<string name="battery">Battery</string>
|
||||
<string name="no_limit">No limit</string>
|
||||
<string name="seconds_5">5 seconds</string>
|
||||
<string name="seconds_6">6 seconds</string>
|
||||
<string name="seconds_7">7 seconds</string>
|
||||
<string name="seconds_8">8 seconds</string>
|
||||
<string name="seconds_9">9 seconds</string>
|
||||
<string name="seconds_10">10 seconds</string>
|
||||
<string name="seconds_11">11 seconds</string>
|
||||
<string name="seconds_12">12 seconds</string>
|
||||
<string name="seconds_13">13 seconds</string>
|
||||
<string name="seconds_14">14 seconds</string>
|
||||
<string name="seconds_15">15 seconds</string>
|
||||
<string name="seconds_20">20 seconds</string>
|
||||
<string name="seconds_30">30 seconds</string>
|
||||
<string name="minutes_1">1 minute</string>
|
||||
@ -705,6 +740,7 @@
|
||||
<string name="miband_prefs_reserve_reminder_calendar">Reminders to reserve for upcoming events</string>
|
||||
<string name="prefs_reserve_reminder_calendar_summary">Number of calendar events that will be synchronized</string>
|
||||
<string name="miband_prefs_hr_sleep_detection">Use heart rate sensor to improve sleep detection</string>
|
||||
<string name="pref_sleep_breathing_quality_monitoring">Sleep breathing quality monitoring</string>
|
||||
<string name="miband_prefs_device_time_offset_hours">Device time offset in hours (for detecting sleep of shift workers)</string>
|
||||
<string name="prefs_find_phone">Find phone</string>
|
||||
<string name="prefs_enable_find_phone">Turn on \'Find phone\'</string>
|
||||
@ -741,6 +777,8 @@
|
||||
<string name="mi2_prefs_inactivity_warnings_dnd_summary">Disable inactivity warnings for a time interval</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring">Heart Rate Monitoring</string>
|
||||
<string name="mi2_prefs_heart_rate_monitoring_summary">Configure heart rate monitoring</string>
|
||||
<string name="prefs_always_on_display">Always On Display</string>
|
||||
<string name="prefs_always_on_display_summary">Keep the band\'s display always on</string>
|
||||
<string name="prefs_password">Password</string>
|
||||
<string name="prefs_password_summary">Lock the band with a password when removed from the wrist</string>
|
||||
<string name="prefs_password_enabled">Password Enabled</string>
|
||||
@ -757,6 +795,7 @@
|
||||
<string name="bip_prefs_shortcuts">Shortcuts</string>
|
||||
<string name="bip_prefs_shotcuts_summary">Choose the shortcuts on the band screen</string>
|
||||
<string name="prefs_activate_display_on_lift_sensitivity">Sensitivity</string>
|
||||
<string name="prefs_screen_timeout">Screen Timeout</string>
|
||||
<string name="mi5_prefs_workout_activity_types">Workout Activity Types</string>
|
||||
<string name="mi5_prefs_workout_activity_types_summary">Choose the activity types to display on the workouts screen</string>
|
||||
<string name="pref_title_force_white_color_scheme">Force black on white color scheme</string>
|
||||
@ -929,10 +968,12 @@
|
||||
<string name="mi2_enable_text_notifications">Text notifications</string>
|
||||
<string name="mi2_enable_text_notifications_summary"><![CDATA[Needs firmware >= 1.0.1.28 and Mili_pro.ft* installed.]]></string>
|
||||
<string name="on">On</string>
|
||||
<string name="smart">Smart</string>
|
||||
<string name="off">Off</string>
|
||||
<string name="normal">Normal</string>
|
||||
<string name="sensitive">Sensitive</string>
|
||||
<string name="mi2_dnd_off">Off</string>
|
||||
<string name="mi2_dnd_always">Always</string>
|
||||
<string name="mi3_night_mode_sunset">At sunset</string>
|
||||
<string name="mi2_dnd_automatic">Automatic (sleep detection)</string>
|
||||
<string name="mi2_dnd_scheduled">Scheduled (time interval)</string>
|
||||
@ -1013,6 +1054,7 @@
|
||||
<string name="devicetype_miband4">Mi Band 4</string>
|
||||
<string name="devicetype_miband5">Mi Band 5</string>
|
||||
<string name="devicetype_miband6">Mi Band 6</string>
|
||||
<string name="devicetype_miband7">Xiaomi Smart Band 7</string>
|
||||
<string name="devicetype_amazfit_band5">Amazfit Band 5</string>
|
||||
<string name="devicetype_amazfit_neo">Amazfit Neo</string>
|
||||
<string name="devicetype_amazfit_bip">Amazfit Bip</string>
|
||||
@ -1125,6 +1167,11 @@
|
||||
<string name="menuitem_barometer">Barometer</string>
|
||||
<string name="menuitem_flashlight">Flashlight</string>
|
||||
<string name='menuitem_email'>E-mail</string>
|
||||
<string name='menuitem_countdown'>Countdown</string>
|
||||
<string name='menuitem_personal_activity_intelligence'>Personal Activity Intelligence</string>
|
||||
<string name='menuitem_workout_history'>Workout History</string>
|
||||
<string name='menuitem_female_health'>Female Health</string>
|
||||
<string name='menuitem_workout_status'>Workout Status</string>
|
||||
<string name="watch9_time_minutes">Minutes:</string>
|
||||
<string name="watch9_time_hours">Hours:</string>
|
||||
<string name="watch9_time_seconds">Seconds:</string>
|
||||
@ -1389,7 +1436,7 @@
|
||||
<string name="medley">Medley</string>
|
||||
<string name="devicetype_nut_mini">Nut mini</string>
|
||||
<string name="qhybrid_calibration_align_hint">Use the buttons below to align the watch hands to 12:00.</string>
|
||||
<string translatable="false" name="lorem_ipsum">Lorem Ipsum</string>
|
||||
<string name="lorem_ipsum" translatable="false">Lorem Ipsum</string>
|
||||
<plurals name="widget_alarm_target_hours">
|
||||
<item quantity="one">%d hour</item>
|
||||
<item quantity="two">%d hours</item>
|
||||
@ -1583,7 +1630,11 @@
|
||||
<string name="pref_voice_detect_duration_5">5 seconds</string>
|
||||
<string name="pref_voice_detect_duration_10">10 seconds</string>
|
||||
<string name="pref_voice_detect_duration_15">15 seconds</string>
|
||||
|
||||
<string name="pref_header_heartrate_sleep">Sleep</string>
|
||||
<string name="pref_header_heartrate_allday">All-day monitoring</string>
|
||||
<string name="pref_header_heartrate_alerts">Heart rate alerts</string>
|
||||
<string name="pref_header_stress">Stress</string>
|
||||
<string name="pref_header_spo2">Blood Oxygen</string>
|
||||
<string name="pref_header_sony_ambient_sound_control">Ambient Sound Control</string>
|
||||
<string name="pref_header_sony_device_info">Device Information</string>
|
||||
<string name="sony_ambient_sound">Mode</string>
|
||||
@ -1670,6 +1721,7 @@
|
||||
<string name="sony_button_mode_ambient_sound_control">Ambient Sound Control</string>
|
||||
<string name="sony_button_mode_playback_control">Playback Control</string>
|
||||
<string name="sony_button_mode_volume_control">Volume Control</string>
|
||||
<string name="pref_screen_brightness">Screen Brightness</string>
|
||||
<string name="watchface_widget_type_custom">Custom widget</string>
|
||||
<string name="watchface_dialog_widget_timezone">Time zone</string>
|
||||
<string name="watchface_dialog_widget_timezone_duration">Clock visibility duration (in seconds)</string>
|
||||
|
@ -55,6 +55,11 @@
|
||||
<item name="p_menuitem_temperature" type="string">temperature</item>
|
||||
<item name="p_menuitem_barometer" type="string">barometer</item>
|
||||
<item name="p_menuitem_flashlight" type="string">flashlight</item>
|
||||
<item name="p_menuitem_countdown" type="string">countdown</item>
|
||||
<item name="p_menuitem_personal_activity_intelligence" type="string">personal_activity_intelligence</item>
|
||||
<item name="p_menuitem_workout_history" type="string">workout_history</item>
|
||||
<item name="p_menuitem_female_health" type="string">female_health</item>
|
||||
<item name="p_menuitem_workout_status" type="string">workout_status</item>
|
||||
|
||||
<!-- currently used for sounds -->
|
||||
<item name="p_menuitem_button" type="string">button</item>
|
||||
@ -66,6 +71,7 @@
|
||||
<item name="p_off" type="string">off</item>
|
||||
<item name="p_on" type="string">on</item>
|
||||
<item name="p_sunset" type="string">sunset</item>
|
||||
<item name="p_always" type="string">always</item>
|
||||
<item name="p_automatic" type="string">automatic</item>
|
||||
<item name="p_scheduled" type="string">scheduled</item>
|
||||
|
||||
|
31
app/src/main/res/xml/devicesettings_always_on_display.xml
Normal file
31
app/src/main/res/xml/devicesettings_always_on_display.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_always_on_display"
|
||||
android:key="pref_screen_always_on_display"
|
||||
android:persistent="false"
|
||||
android:summary="@string/prefs_always_on_display_summary"
|
||||
android:title="@string/prefs_always_on_display">
|
||||
|
||||
<!-- workaround for missing toolbar -->
|
||||
<PreferenceCategory android:title="@string/prefs_always_on_display" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/p_off"
|
||||
android:entries="@array/always_on_display"
|
||||
android:entryValues="@array/always_on_display_values"
|
||||
android:key="always_on_display_mode"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_always_on_display" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="00:00"
|
||||
android:key="always_on_display_start"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_start" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="00:00"
|
||||
android:key="always_on_display_end"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_end" />
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
@ -8,72 +8,94 @@
|
||||
android:title="@string/pref_title_canned_messages_dismisscall"
|
||||
android:summary="@string/pref_summary_canned_messages_dismisscall">
|
||||
<Preference
|
||||
android:icon="@drawable/ic_refresh"
|
||||
android:key="canned_messages_dismisscall_send"
|
||||
android:title="@string/pref_title_canned_messages_set"
|
||||
android:summary="@string/pref_summary_canned_messages_set"/>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_canned_reply_messages"
|
||||
android:title="@string/pref_header_cannned_messages">
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_1"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_2"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_3"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_4"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_5"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_6"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_7"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_8"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_9"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_10"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_11"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_12"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_13"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_14"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_15"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_message_dismisscall_16"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
@ -5,77 +5,105 @@
|
||||
android:icon="@drawable/ic_reply"
|
||||
android:key="screen_canned_messages_reply"
|
||||
android:persistent="false"
|
||||
android:summary="@string/pref_summary_canned_replies"
|
||||
android:title="@string/pref_title_canned_replies">
|
||||
<Preference
|
||||
android:icon="@drawable/ic_refresh"
|
||||
android:key="canned_messages_generic_send"
|
||||
android:title="@string/pref_title_canned_messages_set"
|
||||
android:summary="@string/pref_summary_canned_messages_set"/>
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue=" (canned reply)"
|
||||
android:key="canned_reply_suffix"
|
||||
android:maxLength="64"
|
||||
android:title="@string/pref_title_canned_reply_suffix"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_canned_reply_messages"
|
||||
android:title="@string/pref_header_cannned_messages">
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_1"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_2"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_3"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_4"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_5"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_6"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_7"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_8"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_9"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_10"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_11"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_12"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_13"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_14"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_15"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_message_outline"
|
||||
android:key="canned_reply_16"
|
||||
android:maxLength="64"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
|
11
app/src/main/res/xml/devicesettings_dateformat_2.xml
Normal file
11
app/src/main/res/xml/devicesettings_dateformat_2.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<ListPreference
|
||||
android:defaultValue="YYYY/MM/DD"
|
||||
android:entries="@array/dateformats_2"
|
||||
android:entryValues="@array/dateformats_2_values"
|
||||
android:icon="@drawable/ic_access_time"
|
||||
android:key="dateformat"
|
||||
android:summary="%s"
|
||||
android:title="@string/miband2_prefs_dateformat" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_extension"
|
||||
android:key="screen_events_forwarding"
|
||||
android:persistent="false"
|
||||
android:summary="@string/prefs_events_forwarding_summary"
|
||||
android:title="@string/prefs_events_forwarding_title">
|
||||
|
||||
<PreferenceCategory
|
||||
android:icon="@drawable/ic_nights_stay"
|
||||
android:title="@string/prefs_events_forwarding_fellsleep">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/pref_button_action_disabled_value"
|
||||
android:entries="@array/device_action_options"
|
||||
android:entryValues="@array/device_action_values"
|
||||
android:key="events_forwarding_fellsleep_action_selection"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_events_forwarding_action_title" />
|
||||
<EditTextPreference
|
||||
android:defaultValue="@string/prefs_events_forwarding_fellsleep_broadcast_default_value"
|
||||
android:key="prefs_events_forwarding_fellsleep_broadcast"
|
||||
android:title="@string/prefs_events_forwarding_broadcast_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:icon="@drawable/ic_wb_sunny"
|
||||
android:title="@string/prefs_events_forwarding_wokeup">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/pref_button_action_disabled_value"
|
||||
android:entries="@array/device_action_options"
|
||||
android:entryValues="@array/device_action_values"
|
||||
android:key="events_forwarding_wokeup_action_selection"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_events_forwarding_action_title" />
|
||||
<EditTextPreference
|
||||
android:defaultValue="@string/prefs_events_forwarding_wokeup_broadcast_default_value"
|
||||
android:key="prefs_events_forwarding_wokeup_broadcast"
|
||||
android:title="@string/prefs_events_forwarding_broadcast_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_block"
|
||||
android:key="screen_do_not_disturb"
|
||||
android:persistent="false"
|
||||
android:summary="@string/mi2_prefs_do_not_disturb_summary"
|
||||
android:title="@string/mi2_prefs_do_not_disturb">
|
||||
|
||||
<!-- workaround for missing toolbar -->
|
||||
<PreferenceCategory android:title="@string/mi2_prefs_do_not_disturb" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/p_off"
|
||||
android:entries="@array/do_not_disturb_with_always"
|
||||
android:entryValues="@array/do_not_disturb_with_always_values"
|
||||
android:key="do_not_disturb"
|
||||
android:summary="%s"
|
||||
android:title="@string/mi2_prefs_do_not_disturb" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="01:00"
|
||||
android:key="do_not_disturb_start"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_start" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="06:00"
|
||||
android:key="do_not_disturb_end"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_end" />
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
6
app/src/main/res/xml/devicesettings_header_calendar.xml
Normal file
6
app/src/main/res/xml/devicesettings_header_calendar.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_calendar"
|
||||
android:title="@string/pref_header_calendar" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_connection"
|
||||
android:title="@string/pref_header_connection" />
|
||||
</androidx.preference.PreferenceScreen>
|
6
app/src/main/res/xml/devicesettings_header_display.xml
Normal file
6
app/src/main/res/xml/devicesettings_header_display.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_display"
|
||||
android:title="@string/pref_header_display" />
|
||||
</androidx.preference.PreferenceScreen>
|
6
app/src/main/res/xml/devicesettings_header_health.xml
Normal file
6
app/src/main/res/xml/devicesettings_header_health.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_health"
|
||||
android:title="@string/pref_header_health" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_notifications"
|
||||
android:title="@string/pref_header_notifications" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_other"
|
||||
android:key="pref_header_other"
|
||||
android:title="@string/pref_header_other" />
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_other"
|
||||
android:key="pref_header_system"
|
||||
android:title="@string/pref_header_system" />
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
6
app/src/main/res/xml/devicesettings_header_time.xml
Normal file
6
app/src/main/res/xml/devicesettings_header_time.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_time"
|
||||
android:title="@string/pref_header_time" />
|
||||
</androidx.preference.PreferenceScreen>
|
6
app/src/main/res/xml/devicesettings_header_workout.xml
Normal file
6
app/src/main/res/xml/devicesettings_header_workout.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_workout"
|
||||
android:title="@string/pref_header_workout" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:key="heartrate_monitoring_key"
|
||||
android:persistent="false"
|
||||
android:summary="@string/mi2_prefs_heart_rate_monitoring_alerts_summary"
|
||||
android:title="@string/mi2_prefs_heart_rate_monitoring">
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_heartrate_sleep"
|
||||
android:title="@string/pref_header_heartrate_sleep">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_activity_sleep"
|
||||
android:key="heartrate_sleep_detection"
|
||||
android:title="@string/miband_prefs_hr_sleep_detection" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_activity_sleep"
|
||||
android:key="heartrate_sleep_breathing_quality_monitoring"
|
||||
android:title="@string/pref_sleep_breathing_quality_monitoring" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_heartrate_allday"
|
||||
android:title="@string/pref_header_heartrate_allday">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/prefs_heartrate_measurement_interval_with_smart"
|
||||
android:entryValues="@array/prefs_heartrate_measurement_interval_with_smart_values"
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:key="heartrate_measurement_interval"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_title_heartrate_measurement_interval" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<!-- Heart Rate Alerts -->
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_heartrate_alerts"
|
||||
android:title="@string/pref_header_heartrate_alerts">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/prefs_miband_heartrate_high_alert_threshold_with_off"
|
||||
android:entryValues="@array/prefs_miband_heartrate_high_alert_threshold_with_off_values"
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:key="heartrate_alert_threshold"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_heartrate_alert_high_threshold" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/prefs_miband_heartrate_low_alert_threshold"
|
||||
android:entryValues="@array/prefs_miband_heartrate_low_alert_threshold_values"
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:key="heartrate_alert_low_threshold"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_heartrate_alert_low_threshold" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<!-- Stress Monitoring -->
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_stress"
|
||||
android:title="@string/pref_header_stress">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_mood_bad"
|
||||
android:key="heartrate_stress_monitoring"
|
||||
android:summary="@string/prefs_stress_monitoring_description"
|
||||
android:title="@string/prefs_stress_monitoring_title" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:dependency="heartrate_stress_monitoring"
|
||||
android:key="heartrate_stress_relaxation_reminder"
|
||||
android:summary="@string/prefs_relaxation_reminder_description"
|
||||
android:title="@string/prefs_relaxation_reminder_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<!-- SPO2 -->
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_header_spo2"
|
||||
android:title="@string/pref_header_spo2">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="spo2_all_day_monitoring_enabled"
|
||||
android:summary="@string/prefs_spo2_monitoring_description"
|
||||
android:title="@string/prefs_spo2_monitoring_title" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:dependency="spo2_all_day_monitoring_enabled"
|
||||
android:entries="@array/prefs_spo2_alert_threshold"
|
||||
android:entryValues="@array/prefs_spo2_alert_threshold_values"
|
||||
android:key="spo2_low_alert_threshold"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_spo2_alert_threshold" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_chair"
|
||||
android:key="screen_inactivity"
|
||||
android:persistent="false"
|
||||
android:summary="@string/mi2_prefs_inactivity_warnings_summary"
|
||||
android:title="@string/mi2_prefs_inactivity_warnings">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="inactivity_warnings_enable"
|
||||
android:title="@string/mi2_prefs_inactivity_warnings"
|
||||
android:summary="@string/mi2_prefs_inactivity_warnings_summary" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="06:00"
|
||||
android:dependency="inactivity_warnings_enable"
|
||||
android:key="inactivity_warnings_start"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_start" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="22:00"
|
||||
android:dependency="inactivity_warnings_enable"
|
||||
android:key="inactivity_warnings_end"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_end" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:dependency="inactivity_warnings_enable"
|
||||
android:key="inactivity_warnings_dnd"
|
||||
android:summary="@string/mi2_prefs_inactivity_warnings_dnd_summary"
|
||||
android:title="@string/mi2_prefs_do_not_disturb" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="12:00"
|
||||
android:dependency="inactivity_warnings_dnd"
|
||||
android:key="inactivity_warnings_dnd_start"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_start" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="14:00"
|
||||
android:dependency="inactivity_warnings_dnd"
|
||||
android:key="inactivity_warnings_dnd_end"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_end" />
|
||||
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
13
app/src/main/res/xml/devicesettings_miband7_displayitems.xml
Normal file
13
app/src/main/res/xml/devicesettings_miband7_displayitems.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<com.mobeta.android.dslv.DragSortListPreference
|
||||
android:defaultValue="@array/pref_miband7_display_items_default"
|
||||
android:dialogTitle="@string/mi2_prefs_display_items"
|
||||
android:entries="@array/pref_miband7_display_items"
|
||||
android:entryValues="@array/pref_miband7_display_items_values"
|
||||
android:icon="@drawable/ic_widgets"
|
||||
android:key="display_items_sortable"
|
||||
android:persistent="true"
|
||||
android:summary="@string/mi2_prefs_display_items_summary"
|
||||
android:title="@string/mi2_prefs_display_items" />
|
||||
</androidx.preference.PreferenceScreen>
|
13
app/src/main/res/xml/devicesettings_miband7_shortcuts.xml
Normal file
13
app/src/main/res/xml/devicesettings_miband7_shortcuts.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<com.mobeta.android.dslv.DragSortListPreference
|
||||
android:icon="@drawable/ic_shortcut"
|
||||
android:defaultValue="@array/pref_miband7_shortcuts_default"
|
||||
android:dialogTitle="@string/bip_prefs_shortcuts"
|
||||
android:entries="@array/pref_miband7_shortcuts"
|
||||
android:entryValues="@array/pref_miband7_shortcuts_values"
|
||||
android:key="shortcuts_sortable"
|
||||
android:persistent="true"
|
||||
android:summary="@string/bip_prefs_shotcuts_summary"
|
||||
android:title="@string/bip_prefs_shortcuts" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<SeekBarPreference
|
||||
android:defaultValue="50"
|
||||
android:icon="@drawable/ic_wb_sunny"
|
||||
android:key="screen_brightness"
|
||||
android:max="100"
|
||||
android:title="@string/pref_screen_brightness" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:icon="@drawable/ic_notifications"
|
||||
android:key="screen_on_on_notifications"
|
||||
android:summary="@string/pref_summary_screen_on_on_notifications"
|
||||
android:title="@string/pref_title_screen_on_on_notifications" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<ListPreference
|
||||
android:defaultValue="5"
|
||||
android:entries="@array/screen_timeout_5_to_15"
|
||||
android:entryValues="@array/screen_timeout_5_to_15_values"
|
||||
android:key="screen_timeout"
|
||||
android:icon="@drawable/ic_hourglass_empty"
|
||||
android:summary="%s"
|
||||
android:title="@string/prefs_screen_timeout" />
|
||||
</androidx.preference.PreferenceScreen>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user