1
0
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:
José Rebelo 2022-08-18 22:03:28 +01:00 committed by Gitea
parent dcce900f23
commit ba565df088
103 changed files with 7909 additions and 1002 deletions

View File

@ -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

View File

@ -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")

View File

@ -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);

View File

@ -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;

View File

@ -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() {

View File

@ -184,6 +184,8 @@ public class FindPhoneActivity extends AbstractGBActivity {
stopVibration();
stopSound();
GBApplication.deviceService().onPhoneFound();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
}

View File

@ -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);

View File

@ -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";

View File

@ -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);

View File

@ -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();

View File

@ -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,

View File

@ -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);

View File

@ -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() {

View File

@ -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;

View File

@ -92,6 +92,8 @@ public interface EventHandler {
void onFindDevice(boolean start);
void onPhoneFound();
void onSetConstantVibration(int integer);
void onScreenshotReq();

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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 };

View File

@ -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;
}
}

View File

@ -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");
}
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -19,5 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband;
public enum DoNotDisturb {
OFF,
AUTOMATIC,
SCHEDULED
SCHEDULED,
ALWAYS,
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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";

View File

@ -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),

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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:

View File

@ -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")) {

View File

@ -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) {

View File

@ -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),

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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++;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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);
}};
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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),

View File

@ -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);

View File

@ -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();
}
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)) {

View File

@ -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");
}

View File

@ -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);
}
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);
LOG.info("Unhandled auth payload: {}", GB.hexdump(payload));
return;
}
}
return true;
}
huamiSupport.logMessageContent(value);
return super.onCharacteristicChanged(gatt, characteristic);
} else {
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
return super.onCharacteristicChanged(gatt, characteristic);
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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());

View File

@ -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();

View File

@ -110,6 +110,10 @@ public abstract class GBDeviceProtocol {
return null;
}
public byte[] encodePhoneFound() {
return null;
}
public byte[] encodeEnableRealtimeSteps(boolean enable) {
return null;
}

View File

@ -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();
}
}

View File

@ -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());

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View 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>

View 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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View 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_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>

View 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>

View 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_connection"
android:title="@string/pref_header_connection" />
</androidx.preference.PreferenceScreen>

View 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>

View 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>

View 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_notifications"
android:title="@string/pref_header_notifications" />
</androidx.preference.PreferenceScreen>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View 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="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