mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-07-04 12:02:06 +02:00
HuamiSupport handles configurations with performInitialize, which may trigger a device reinitialization if called while the device is already initializing. Handle fitness goals in Huami2021Support, which should be one of the last settings still missing.
1815 lines
74 KiB
Java
1815 lines
74 KiB
Java
/* 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.zeppos.services;
|
|
|
|
import static org.apache.commons.lang3.ArrayUtils.subarray;
|
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
|
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.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 android.widget.Toast;
|
|
|
|
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.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.HashSet;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.GpsCapability;
|
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.WorkoutDetectionCapability;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
|
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.huami.Huami2021Coordinator;
|
|
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.service.devices.huami.Huami2021MenuType;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.MapUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
|
|
|
public class ZeppOsConfigService extends AbstractZeppOsService {
|
|
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsConfigService.class);
|
|
|
|
private static final short ENDPOINT = 0x000a;
|
|
|
|
private static final byte CMD_CAPABILITIES_REQUEST = 0x01;
|
|
private static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
|
|
private static final byte CMD_REQUEST = 0x03;
|
|
private static final byte CMD_RESPONSE = 0x04;
|
|
private static final byte CMD_SET = 0x05;
|
|
private static final byte CMD_ACK = 0x06;
|
|
|
|
private final Map<ConfigGroup, Byte> mGroupVersions = new HashMap<>();
|
|
|
|
public ZeppOsConfigService(final Huami2021Support support) {
|
|
super(support);
|
|
}
|
|
|
|
@Override
|
|
public short getEndpoint() {
|
|
return ENDPOINT;
|
|
}
|
|
|
|
@Override
|
|
public boolean isEncrypted() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void handlePayload(final byte[] payload) {
|
|
switch (payload[0]) {
|
|
case CMD_ACK:
|
|
LOG.info("Configuration ACK, status = {}", payload[1]);
|
|
return;
|
|
|
|
case CMD_RESPONSE:
|
|
if (payload[1] != 1) {
|
|
LOG.warn("Configuration response not success: {}", payload[1]);
|
|
return;
|
|
}
|
|
|
|
handle2021ConfigResponse(payload);
|
|
return;
|
|
default:
|
|
LOG.warn("Unexpected configuration payload byte {}", String.format("0x%02x", payload[0]));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void initialize(TransactionBuilder builder) {
|
|
requestAllConfigs(builder);
|
|
}
|
|
|
|
@Override
|
|
public boolean onSendConfiguration(final String prefKey, Prefs prefs) {
|
|
if (!PREF_TO_CONFIG.containsKey(prefKey)) {
|
|
return false;
|
|
}
|
|
|
|
final ConfigSetter configSetter = new ConfigSetter();
|
|
if (setConfig(prefs, prefKey, configSetter)) {
|
|
try {
|
|
// If the ConfigSetter was able to set the config, just write it and return
|
|
final TransactionBuilder builder = new TransactionBuilder("send config " + prefKey);
|
|
configSetter.write(builder);
|
|
builder.queue(getSupport().getQueue());
|
|
} catch (final Exception e) {
|
|
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean sentFitnessGoal = false;
|
|
|
|
private void handle2021ConfigResponse(final byte[] payload) {
|
|
final ConfigGroup configGroup = ConfigGroup.fromValue(payload[2]);
|
|
if (configGroup == null) {
|
|
LOG.warn("Unknown config type {}", String.format("0x%02x", payload[2]));
|
|
return;
|
|
}
|
|
|
|
final byte version = payload[3];
|
|
if (configGroup.getVersion() != version) {
|
|
// Special case for HEALTH, where we actually support version 1 as well
|
|
// TODO: Support multiple versions in a cleaner way...
|
|
if (!(configGroup == ConfigGroup.HEALTH && configGroup.getVersion() == 1)) {
|
|
LOG.warn("Unexpected version {} for {}", String.format("0x%02x", version), configGroup);
|
|
return;
|
|
}
|
|
}
|
|
mGroupVersions.put(configGroup, version);
|
|
|
|
final boolean includesConstraints = payload[4] == 0x01;
|
|
|
|
int numConfigs = payload[5] & 0xff;
|
|
|
|
LOG.info("Got {} configs for {} version {}", numConfigs, configGroup, version);
|
|
|
|
final Map<String, Object> prefs = new ZeppOsConfigService.ConfigParser(configGroup, includesConstraints)
|
|
.parse(numConfigs, subarray(payload, 6, payload.length));
|
|
|
|
if (prefs == null) {
|
|
return;
|
|
}
|
|
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(prefs);
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
|
|
if (getSupport().getDevice().isInitialized()) {
|
|
if (prefs.containsKey(PREF_LANGUAGE) && prefs.get(PREF_LANGUAGE).equals(PREF_LANGUAGE_AUTO)) {
|
|
// Band is reporting automatic language, we need to send the actual language
|
|
getSupport().onSendConfiguration(PREF_LANGUAGE);
|
|
}
|
|
if (prefs.containsKey(PREF_TIMEFORMAT) && prefs.get(PREF_TIMEFORMAT).equals(PREF_TIMEFORMAT_AUTO)) {
|
|
// Band is reporting automatic time format, we need to send the actual time format
|
|
getSupport().onSendConfiguration(PREF_TIMEFORMAT);
|
|
}
|
|
}
|
|
|
|
if (configGroup == ConfigGroup.HEALTH && !sentFitnessGoal) {
|
|
// We need to send the fitness goal after we got the protocol version
|
|
getSupport().onSendConfiguration(PREF_USER_FITNESS_GOAL);
|
|
sentFitnessGoal = true;
|
|
}
|
|
}
|
|
|
|
public void requestAllConfigs(final TransactionBuilder builder) {
|
|
for (final ConfigGroup configGroup : ConfigGroup.values()) {
|
|
requestConfig(builder, configGroup);
|
|
}
|
|
}
|
|
|
|
public void requestConfig(final TransactionBuilder builder, final ConfigGroup config) {
|
|
requestConfig(builder, config, true, ZeppOsConfigService.ConfigArg.getAllArgsForConfigGroup(config));
|
|
}
|
|
|
|
public void requestConfig(final TransactionBuilder builder,
|
|
final ConfigGroup config,
|
|
final boolean includeConstraints,
|
|
final List<ZeppOsConfigService.ConfigArg> args) {
|
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
|
|
baos.write(CMD_REQUEST);
|
|
baos.write((byte) (includeConstraints ? 1 : 0));
|
|
baos.write(config.getValue());
|
|
baos.write(args.size());
|
|
for (final ZeppOsConfigService.ConfigArg arg : args) {
|
|
baos.write(arg.getCode());
|
|
}
|
|
|
|
write(builder, baos.toByteArray());
|
|
}
|
|
|
|
public enum ConfigGroup {
|
|
AGPS(0x00, 0x01),
|
|
DISPLAY(0x01, 0x02),
|
|
// TODO 0x02
|
|
SOUND_AND_VIBRATION(0x03, 0x02),
|
|
LOCKSCREEN(0x04, 0x01),
|
|
WEARING_DIRECTION(0x05, 0x02),
|
|
OFFLINE_VOICE(0x06, 0x02),
|
|
LANGUAGE(0x07, 0x02),
|
|
HEALTH(0x08, 0x02),
|
|
WORKOUT(0x09, 0x01),
|
|
SYSTEM(0x0a, 0x01),
|
|
BLUETOOTH(0x0b, 0x01),
|
|
;
|
|
|
|
private final byte value;
|
|
private final byte version;
|
|
|
|
ConfigGroup(int value, int version) {
|
|
this.value = (byte) value;
|
|
this.version = (byte) version;
|
|
}
|
|
|
|
public byte getValue() {
|
|
return value;
|
|
}
|
|
|
|
public byte getVersion() {
|
|
return version;
|
|
}
|
|
|
|
public static ConfigGroup fromValue(final byte value) {
|
|
for (final ConfigGroup configGroup : values()) {
|
|
if (configGroup.getValue() == value) {
|
|
return configGroup;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public enum ConfigType {
|
|
BOOL(0x0b),
|
|
STRING(0x20),
|
|
STRING_LIST(0x21),
|
|
SHORT(0x01),
|
|
INT(0x03),
|
|
BYTE(0x10),
|
|
BYTE_LIST(0x11),
|
|
DATETIME_HH_MM(0x30),
|
|
TIMESTAMP_MILLIS(0x40),
|
|
;
|
|
|
|
private final byte value;
|
|
|
|
ConfigType(int value) {
|
|
this.value = (byte) value;
|
|
}
|
|
|
|
public byte getValue() {
|
|
return value;
|
|
}
|
|
|
|
public static ConfigType fromValue(final byte value) {
|
|
for (final ConfigType configType : values()) {
|
|
if (configType.getValue() == value) {
|
|
return configType;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public enum ConfigArg {
|
|
// AGPS
|
|
AGPS_UPDATE_TIME(ConfigGroup.AGPS, ConfigType.TIMESTAMP_MILLIS, 0x09, PREF_AGPS_UPDATE_TIME),
|
|
AGPS_EXPIRE_TIME(ConfigGroup.AGPS, ConfigType.TIMESTAMP_MILLIS, 0x0a, PREF_AGPS_EXPIRE_TIME),
|
|
|
|
// Display
|
|
SCREEN_AUTO_BRIGHTNESS(ConfigGroup.DISPLAY, ConfigType.BOOL, 0x01, PREF_SCREEN_AUTO_BRIGHTNESS),
|
|
SCREEN_BRIGHTNESS(ConfigGroup.DISPLAY, ConfigType.SHORT, 0x02, PREF_SCREEN_BRIGHTNESS),
|
|
SCREEN_TIMEOUT(ConfigGroup.DISPLAY, ConfigType.BYTE, 0x03, PREF_SCREEN_TIMEOUT),
|
|
ALWAYS_ON_DISPLAY_MODE(ConfigGroup.DISPLAY, ConfigType.BYTE, 0x04, PREF_ALWAYS_ON_DISPLAY_MODE),
|
|
ALWAYS_ON_DISPLAY_SCHEDULED_START(ConfigGroup.DISPLAY, ConfigType.DATETIME_HH_MM, 0x05, PREF_ALWAYS_ON_DISPLAY_START),
|
|
ALWAYS_ON_DISPLAY_SCHEDULED_END(ConfigGroup.DISPLAY, ConfigType.DATETIME_HH_MM, 0x06, PREF_ALWAYS_ON_DISPLAY_END),
|
|
LIFT_WRIST_MODE(ConfigGroup.DISPLAY, ConfigType.BYTE, 0x08, PREF_ACTIVATE_DISPLAY_ON_LIFT),
|
|
LIFT_WRIST_SCHEDULED_START(ConfigGroup.DISPLAY, ConfigType.DATETIME_HH_MM, 0x09, PREF_DISPLAY_ON_LIFT_START),
|
|
LIFT_WRIST_SCHEDULED_END(ConfigGroup.DISPLAY, ConfigType.DATETIME_HH_MM, 0x0a, PREF_DISPLAY_ON_LIFT_END),
|
|
LIFT_WRIST_RESPONSE_SENSITIVITY(ConfigGroup.DISPLAY, ConfigType.BYTE, 0x0b, PREF_DISPLAY_ON_LIFT_SENSITIVITY),
|
|
SCREEN_ON_ON_NOTIFICATIONS(ConfigGroup.DISPLAY, ConfigType.BOOL, 0x0c, PREF_SCREEN_ON_ON_NOTIFICATIONS),
|
|
WORKOUT_KEEP_SCREEN_ON(ConfigGroup.DISPLAY, ConfigType.BOOL, 0x0d, PREF_WORKOUT_KEEP_SCREEN_ON),
|
|
ALWAYS_ON_DISPLAY_FOLLOW_WATCHFACE(ConfigGroup.DISPLAY, ConfigType.BOOL, 0x0e, PREF_ALWAYS_ON_DISPLAY_FOLLOW_WATCHFACE),
|
|
ALWAYS_ON_DISPLAY_STYLE(ConfigGroup.DISPLAY, ConfigType.STRING_LIST, 0x0f, PREF_ALWAYS_ON_DISPLAY_STYLE),
|
|
|
|
// Sound and Vibration
|
|
VOLUME(ConfigGroup.SOUND_AND_VIBRATION, ConfigType.SHORT, 0x02, PREF_VOLUME),
|
|
CROWN_VIBRATION(ConfigGroup.SOUND_AND_VIBRATION, ConfigType.BOOL, 0x06, PREF_CROWN_VIBRATION),
|
|
ALERT_TONE(ConfigGroup.SOUND_AND_VIBRATION, ConfigType.BOOL, 0x07, PREF_ALERT_TONE),
|
|
COVER_TO_MUTE(ConfigGroup.SOUND_AND_VIBRATION, ConfigType.BOOL, 0x08, PREF_COVER_TO_MUTE),
|
|
VIBRATE_FOR_ALERT(ConfigGroup.SOUND_AND_VIBRATION, ConfigType.BOOL, 0x09, PREF_VIBRATE_FOR_ALERT),
|
|
TEXT_TO_SPEECH(ConfigGroup.SOUND_AND_VIBRATION, ConfigType.BOOL, 0x0a, PREF_TEXT_TO_SPEECH),
|
|
|
|
// Wearing Direction
|
|
WEARING_DIRECTION_BUTTONS(ConfigGroup.WEARING_DIRECTION, ConfigType.BYTE, 0x02, PREF_WEARDIRECTION),
|
|
|
|
// Offline Voice
|
|
OFFLINE_VOICE_RESPOND_TURN_WRIST(ConfigGroup.OFFLINE_VOICE, ConfigType.BOOL, 0x01, PREF_OFFLINE_VOICE_RESPOND_TURN_WRIST),
|
|
OFFLINE_VOICE_RESPOND_SCREEN_ON(ConfigGroup.OFFLINE_VOICE, ConfigType.BOOL, 0x02, PREF_OFFLINE_VOICE_RESPOND_SCREEN_ON),
|
|
OFFLINE_VOICE_RESPONSE_DURING_SCREEN_LIGHTING(ConfigGroup.OFFLINE_VOICE, ConfigType.BOOL, 0x03, PREF_OFFLINE_VOICE_RESPONSE_DURING_SCREEN_LIGHTING),
|
|
OFFLINE_VOICE_LANGUAGE(ConfigGroup.OFFLINE_VOICE, ConfigType.BYTE, 0x04, PREF_OFFLINE_VOICE_LANGUAGE),
|
|
|
|
// Lock Screen
|
|
PASSWORD_ENABLED(ConfigGroup.LOCKSCREEN, ConfigType.BOOL, 0x01, PREF_PASSWORD_ENABLED),
|
|
PASSWORD_TEXT(ConfigGroup.LOCKSCREEN, ConfigType.STRING, 0x02, PREF_PASSWORD),
|
|
|
|
// Language
|
|
LANGUAGE(ConfigGroup.LANGUAGE, ConfigType.BYTE, 0x01, PREF_LANGUAGE),
|
|
LANGUAGE_FOLLOW_PHONE(ConfigGroup.LANGUAGE, ConfigType.BOOL, 0x02, null /* special case, handled below */),
|
|
|
|
// Health
|
|
HEART_RATE_ALL_DAY_MONITORING(ConfigGroup.HEALTH, ConfigType.BYTE, 0x01, PREF_HEARTRATE_MEASUREMENT_INTERVAL),
|
|
HEART_RATE_HIGH_ALERTS(ConfigGroup.HEALTH, ConfigType.BYTE, 0x02, PREF_HEARTRATE_ALERT_HIGH_THRESHOLD),
|
|
HEART_RATE_LOW_ALERTS(ConfigGroup.HEALTH, ConfigType.BYTE, 0x03, PREF_HEARTRATE_ALERT_LOW_THRESHOLD),
|
|
HEART_RATE_ACTIVITY_MONITORING(ConfigGroup.HEALTH, ConfigType.BOOL, 0x04, PREF_HEARTRATE_ACTIVITY_MONITORING),
|
|
THIRD_PARTY_HR_SHARING(ConfigGroup.HEALTH, ConfigType.BOOL, 0x05, PREF_EXPOSE_HR_THIRDPARTY),
|
|
SLEEP_HIGH_ACCURACY_MONITORING(ConfigGroup.HEALTH, ConfigType.BOOL, 0x11, PREF_HEARTRATE_USE_FOR_SLEEP_DETECTION),
|
|
SLEEP_BREATHING_QUALITY_MONITORING(ConfigGroup.HEALTH, ConfigType.BOOL, 0x12, PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING),
|
|
STRESS_MONITORING(ConfigGroup.HEALTH, ConfigType.BOOL, 0x13, PREF_HEARTRATE_STRESS_MONITORING),
|
|
STRESS_RELAXATION_REMINDER(ConfigGroup.HEALTH, ConfigType.BOOL, 0x14, PREF_HEARTRATE_STRESS_RELAXATION_REMINDER),
|
|
SPO2_ALL_DAY_MONITORING(ConfigGroup.HEALTH, ConfigType.BOOL, 0x31, PREF_SPO2_ALL_DAY_MONITORING),
|
|
SPO2_LOW_ALERT(ConfigGroup.HEALTH, ConfigType.BYTE, 0x32, PREF_SPO2_LOW_ALERT_THRESHOLD),
|
|
FITNESS_GOAL_NOTIFICATION(ConfigGroup.HEALTH, ConfigType.BOOL, 0x51, PREF_USER_FITNESS_GOAL_NOTIFICATION),
|
|
FITNESS_GOAL_STEPS(ConfigGroup.HEALTH, null /* Special case, handled below */, 0x52, null), // TODO needs to be handled globally
|
|
FITNESS_GOAL_CALORIES(ConfigGroup.HEALTH, ConfigType.SHORT, 0x53, null), // TODO needs to be handled globally
|
|
FITNESS_GOAL_WEIGHT(ConfigGroup.HEALTH, ConfigType.SHORT, 0x54, null), // TODO needs to be handled globally
|
|
FITNESS_GOAL_SLEEP(ConfigGroup.HEALTH, ConfigType.SHORT, 0x55, null), // TODO needs to be handled globally
|
|
FITNESS_GOAL_STANDING_TIME(ConfigGroup.HEALTH, ConfigType.SHORT, 0x56, null), // TODO needs to be handled globally
|
|
FITNESS_GOAL_FAT_BURN_TIME(ConfigGroup.HEALTH, ConfigType.SHORT, 0x57, null), // TODO needs to be handled globally
|
|
INACTIVITY_WARNINGS_ENABLED(ConfigGroup.HEALTH, ConfigType.BOOL, 0x41, PREF_INACTIVITY_ENABLE),
|
|
INACTIVITY_WARNINGS_SCHEDULED_START(ConfigGroup.HEALTH, ConfigType.DATETIME_HH_MM, 0x42, PREF_INACTIVITY_START),
|
|
INACTIVITY_WARNINGS_SCHEDULED_END(ConfigGroup.HEALTH, ConfigType.DATETIME_HH_MM, 0x43, PREF_INACTIVITY_END),
|
|
INACTIVITY_WARNINGS_DND_ENABLED(ConfigGroup.HEALTH, ConfigType.BOOL, 0x44, PREF_INACTIVITY_DND),
|
|
INACTIVITY_WARNINGS_DND_SCHEDULED_START(ConfigGroup.HEALTH, ConfigType.DATETIME_HH_MM, 0x45, PREF_INACTIVITY_DND_START),
|
|
INACTIVITY_WARNINGS_DND_SCHEDULED_END(ConfigGroup.HEALTH, ConfigType.DATETIME_HH_MM, 0x46, PREF_INACTIVITY_DND_END),
|
|
|
|
// Workout
|
|
WORKOUT_GPS_PRESET(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x20, PREF_GPS_MODE_PRESET),
|
|
WORKOUT_GPS_BAND(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x21, PREF_GPS_BAND),
|
|
WORKOUT_GPS_COMBINATION(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x22, PREF_GPS_COMBINATION),
|
|
WORKOUT_GPS_SATELLITE_SEARCH(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x23, PREF_GPS_SATELLITE_SEARCH),
|
|
WORKOUT_AGPS_EXPIRY_REMINDER_ENABLED(ConfigGroup.WORKOUT, ConfigType.BOOL, 0x30, PREF_AGPS_EXPIRY_REMINDER_ENABLED),
|
|
WORKOUT_AGPS_EXPIRY_REMINDER_TIME(ConfigGroup.WORKOUT, ConfigType.DATETIME_HH_MM, 0x31, PREF_AGPS_EXPIRY_REMINDER_TIME),
|
|
WORKOUT_DETECTION_CATEGORY(ConfigGroup.WORKOUT, ConfigType.BYTE_LIST, 0x40, PREF_WORKOUT_DETECTION_CATEGORIES),
|
|
WORKOUT_DETECTION_ALERT(ConfigGroup.WORKOUT, ConfigType.BOOL, 0x41, PREF_WORKOUT_DETECTION_ALERT),
|
|
WORKOUT_DETECTION_SENSITIVITY(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x42, PREF_WORKOUT_DETECTION_SENSITIVITY),
|
|
WORKOUT_POOL_SWIMMING_SIZE(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x51, null), // TODO ?
|
|
|
|
// System
|
|
TIME_FORMAT(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x01, PREF_TIMEFORMAT),
|
|
DATE_FORMAT(ConfigGroup.SYSTEM, ConfigType.STRING, 0x02, PREF_DATEFORMAT),
|
|
DND_MODE(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x0a, PREF_DO_NOT_DISTURB),
|
|
DND_SCHEDULED_START(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x0b, PREF_DO_NOT_DISTURB_START),
|
|
DND_SCHEDULED_END(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x0c, PREF_DO_NOT_DISTURB_END),
|
|
CALL_DELAY(ConfigGroup.SYSTEM, ConfigType.SHORT, 0x11, PREF_NOTIFICATION_DELAY_CALLS),
|
|
TEMPERATURE_UNIT(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x12, SettingsActivity.PREF_MEASUREMENT_SYSTEM),
|
|
TIME_FORMAT_FOLLOWS_PHONE(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x13, null /* special case, handled below */),
|
|
UPPER_BUTTON_LONG_PRESS(ConfigGroup.SYSTEM, ConfigType.STRING_LIST, 0x15, PREF_UPPER_BUTTON_LONG_PRESS),
|
|
LOWER_BUTTON_PRESS(ConfigGroup.SYSTEM, ConfigType.STRING_LIST, 0x16, PREF_LOWER_BUTTON_SHORT_PRESS),
|
|
DISPLAY_CALLER(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x18, PREF_DISPLAY_CALLER),
|
|
NIGHT_MODE_MODE(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x1b, PREF_NIGHT_MODE),
|
|
NIGHT_MODE_SCHEDULED_START(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x1c, PREF_NIGHT_MODE_START),
|
|
NIGHT_MODE_SCHEDULED_END(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x1d, PREF_NIGHT_MODE_END),
|
|
SLEEP_MODE_SLEEP_SCREEN(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x21, PREF_SLEEP_MODE_SLEEP_SCREEN),
|
|
SLEEP_MODE_SMART_ENABLE(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x22, PREF_SLEEP_MODE_SMART_ENABLE),
|
|
CAMERA_REMOTE(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x23, PREF_CAMERA_REMOTE),
|
|
|
|
// Bluetooth
|
|
BLUETOOTH_CONNECTED_ADVERTISING(ConfigGroup.BLUETOOTH, ConfigType.BOOL, 0x02, PREF_BT_CONNECTED_ADVERTISEMENT),
|
|
;
|
|
|
|
private final ConfigGroup configGroup;
|
|
private final ConfigType configType;
|
|
private final byte code;
|
|
private final String prefKey;
|
|
|
|
ConfigArg(final ConfigGroup configGroup, final ConfigType configType, final int code, final String prefKey) {
|
|
this.configGroup = configGroup;
|
|
this.configType = configType;
|
|
this.code = (byte) code;
|
|
this.prefKey = prefKey;
|
|
}
|
|
|
|
public ConfigGroup getConfigGroup() {
|
|
return configGroup;
|
|
}
|
|
|
|
public ConfigType getConfigType(@Nullable final Map<ConfigGroup, Byte> groupVersions) {
|
|
if (this == FITNESS_GOAL_STEPS) {
|
|
if (groupVersions == null) {
|
|
return ConfigType.INT;
|
|
}
|
|
|
|
final Byte groupVersion = groupVersions.get(getConfigGroup());
|
|
if (groupVersion == null) {
|
|
LOG.error("Version for {} is not known", getConfigGroup());
|
|
return null;
|
|
}
|
|
|
|
switch (groupVersion) {
|
|
case 0x01:
|
|
return ConfigType.SHORT;
|
|
case 0x02:
|
|
default:
|
|
return ConfigType.INT;
|
|
}
|
|
}
|
|
|
|
return configType;
|
|
}
|
|
|
|
public byte getCode() {
|
|
return code;
|
|
}
|
|
|
|
public String getPrefKey() {
|
|
return prefKey;
|
|
}
|
|
|
|
public static ConfigArg fromCode(final ConfigGroup configGroup, final byte code) {
|
|
for (final ZeppOsConfigService.ConfigArg arg : values()) {
|
|
if (arg.getConfigGroup().equals(configGroup) && arg.getCode() == code) {
|
|
return arg;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static List<ConfigArg> getAllArgsForConfigGroup(final ConfigGroup configGroup) {
|
|
final List<ZeppOsConfigService.ConfigArg> configArgs = new ArrayList<>();
|
|
for (final ZeppOsConfigService.ConfigArg arg : values()) {
|
|
if (arg.getConfigGroup().equals(configGroup)) {
|
|
configArgs.add(arg);
|
|
}
|
|
}
|
|
return configArgs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map of pref key to config.
|
|
*/
|
|
private static final Map<String, ConfigArg> PREF_TO_CONFIG = new HashMap<String, ConfigArg>() {{
|
|
for (final ConfigArg arg : ConfigArg.values()) {
|
|
if (arg.getPrefKey() != null) {
|
|
if (containsKey(arg.getPrefKey())) {
|
|
LOG.error("Duplicate config preference key: {}", arg);
|
|
continue;
|
|
}
|
|
put(arg.getPrefKey(), arg);
|
|
}
|
|
}
|
|
}};
|
|
|
|
/**
|
|
* Updates a {@link ConfigSetter} with a preference. Default values don't really matter - if we're
|
|
* setting this preference, it's because the device reported it, along with the current value, so we
|
|
* shouldn't need a default unless there's a bug.
|
|
*
|
|
* @return true if the {@link ConfigSetter} was updated for this preference key
|
|
*/
|
|
public boolean setConfig(final Prefs prefs, final String key, final ConfigSetter setter) {
|
|
final ConfigArg configArg = PREF_TO_CONFIG.get(key);
|
|
if (configArg == null) {
|
|
LOG.error("Unknown pref key {}", key);
|
|
return false;
|
|
}
|
|
|
|
switch (configArg.getConfigType(mGroupVersions)) {
|
|
case BOOL:
|
|
setter.setBoolean(configArg, prefs.getBoolean(key, false));
|
|
return true;
|
|
case STRING:
|
|
final String encodedString = encodeString(configArg, prefs.getString(key, null));
|
|
if (encodedString != null) {
|
|
setter.setString(configArg, encodedString);
|
|
return true;
|
|
}
|
|
break;
|
|
case STRING_LIST:
|
|
final String encodedStringList = encodeString(configArg, prefs.getString(key, null));
|
|
if (encodedStringList != null) {
|
|
setter.setStringList(configArg, encodedStringList);
|
|
return true;
|
|
}
|
|
break;
|
|
case SHORT:
|
|
setter.setShort(configArg, (short) prefs.getInt(key, 0));
|
|
return true;
|
|
case INT:
|
|
setter.setInt(configArg, prefs.getInt(key, 0));
|
|
return true;
|
|
case BYTE:
|
|
final Byte encodedByte = encodeByte(configArg, prefs.getString(key, null));
|
|
if (encodedByte != null) {
|
|
setter.setByte(configArg, encodedByte);
|
|
return true;
|
|
}
|
|
break;
|
|
case BYTE_LIST:
|
|
final Set<String> byteListString = prefs.getStringSet(key, Collections.emptySet());
|
|
final byte[] encodedByteList = new byte[byteListString.size()];
|
|
int i = 0;
|
|
for (final String s : byteListString) {
|
|
encodedByteList[i++] = encodeByte(configArg, s);
|
|
}
|
|
setter.setByteList(configArg, encodedByteList);
|
|
return true;
|
|
case DATETIME_HH_MM:
|
|
setter.setHourMinute(configArg, prefs.getTimePreference(key, "00:00"));
|
|
return true;
|
|
}
|
|
|
|
LOG.warn("Failed to set {}", configArg);
|
|
|
|
return false;
|
|
}
|
|
|
|
private static String encodeString(final ConfigArg configArg, final String value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
|
|
switch (configArg) {
|
|
case UPPER_BUTTON_LONG_PRESS:
|
|
case LOWER_BUTTON_PRESS:
|
|
return MapUtils.reverse(Huami2021MenuType.displayItemNameLookup).get(value);
|
|
case DATE_FORMAT:
|
|
return value.replace("/", ".");
|
|
}
|
|
|
|
return value; // passthrough
|
|
}
|
|
|
|
private static Byte encodeByte(final ConfigArg configArg, final String value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
|
|
switch (configArg) {
|
|
case ALWAYS_ON_DISPLAY_MODE:
|
|
return encodeEnum(ALWAYS_ON_DISPLAY_MAP, value);
|
|
case LIFT_WRIST_MODE:
|
|
return encodeEnum(LIFT_WRIST_MAP, value);
|
|
case LIFT_WRIST_RESPONSE_SENSITIVITY:
|
|
return encodeEnum(LIFT_WRIST_SENSITIVITY_MAP, value);
|
|
case LANGUAGE:
|
|
return languageLocaleToByte(value);
|
|
case HEART_RATE_ALL_DAY_MONITORING:
|
|
return encodeHeartRateAllDayMonitoring(value);
|
|
case SCREEN_TIMEOUT:
|
|
case HEART_RATE_HIGH_ALERTS:
|
|
case HEART_RATE_LOW_ALERTS:
|
|
case SPO2_LOW_ALERT:
|
|
return (byte) Integer.parseInt(value);
|
|
case TIME_FORMAT:
|
|
return encodeString(TIME_FORMAT_MAP, value);
|
|
case DND_MODE:
|
|
return encodeEnum(DND_MODE_MAP, value);
|
|
case TEMPERATURE_UNIT:
|
|
return encodeEnum(TEMPERATURE_UNIT_MAP, value);
|
|
case NIGHT_MODE_MODE:
|
|
return encodeString(NIGHT_MODE_MAP, value);
|
|
case WEARING_DIRECTION_BUTTONS:
|
|
return encodeString(WEARING_DIRECTION_MAP, value);
|
|
case OFFLINE_VOICE_LANGUAGE:
|
|
return encodeString(OFFLINE_VOICE_LANGUAGE_MAP, value);
|
|
case WORKOUT_GPS_PRESET:
|
|
return encodeEnum(GPS_PRESET_MAP, value);
|
|
case WORKOUT_GPS_BAND:
|
|
return encodeEnum(GPS_BAND_MAP, value);
|
|
case WORKOUT_GPS_COMBINATION:
|
|
return encodeEnum(GPS_COMBINATION_MAP, value);
|
|
case WORKOUT_GPS_SATELLITE_SEARCH:
|
|
return encodeEnum(GPS_SATELLITE_SEARCH_MAP, value);
|
|
case WORKOUT_DETECTION_CATEGORY:
|
|
return encodeEnum(WORKOUT_DETECTION_CATEGORY_MAP, value);
|
|
case WORKOUT_DETECTION_SENSITIVITY:
|
|
return encodeEnum(WORKOUT_DETECTION_SENSITIVITY_MAP, value);
|
|
}
|
|
|
|
LOG.error("No encoder for {}", configArg);
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the preference key where to save the minimum possible value for a preference.
|
|
*/
|
|
public static String getPrefMinKey(final String key) {
|
|
return String.format(Locale.ROOT, "%s_huami_2021_min", key);
|
|
}
|
|
|
|
/**
|
|
* Returns the preference key where to save the maximum possible value for a preference.
|
|
*/
|
|
public static String getPrefMaxKey(final String key) {
|
|
return String.format(Locale.ROOT, "%s_huami_2021_max", key);
|
|
}
|
|
|
|
public static boolean deviceHasConfig(final Prefs devicePrefs, final ZeppOsConfigService.ConfigArg config) {
|
|
return devicePrefs.getBoolean(Huami2021Coordinator.getPrefKnownConfig(config.name()), false);
|
|
}
|
|
|
|
public ConfigSetter newSetter() {
|
|
return new ConfigSetter();
|
|
}
|
|
|
|
public class ConfigSetter {
|
|
private final Map<ConfigGroup, Map<ConfigArg, byte[]>> arguments = new LinkedHashMap<>();
|
|
|
|
public ConfigSetter() {
|
|
}
|
|
|
|
public ConfigSetter setBoolean(final ConfigArg arg, final boolean value) {
|
|
checkArg(arg, ConfigType.BOOL);
|
|
|
|
putArgument(arg, new byte[]{(byte) (value ? 0x01 : 0x00)});
|
|
|
|
return this;
|
|
}
|
|
|
|
public ConfigSetter setString(final ConfigArg arg, final String value) {
|
|
checkArg(arg, ConfigType.STRING);
|
|
|
|
putArgument(arg, (value + "\0").getBytes(StandardCharsets.UTF_8));
|
|
|
|
return this;
|
|
}
|
|
|
|
public ConfigSetter setStringList(final ConfigArg arg, final String value) {
|
|
checkArg(arg, ConfigType.STRING_LIST);
|
|
|
|
putArgument(arg, (value + "\0").getBytes(StandardCharsets.UTF_8));
|
|
|
|
return this;
|
|
}
|
|
|
|
public ConfigSetter setShort(final ConfigArg arg, final short value) {
|
|
checkArg(arg, ConfigType.SHORT);
|
|
|
|
putArgument(arg, BLETypeConversions.fromUint16(value));
|
|
|
|
return this;
|
|
}
|
|
|
|
public ConfigSetter setInt(final ConfigArg arg, final int value) {
|
|
checkArg(arg, ConfigType.INT);
|
|
|
|
putArgument(arg, BLETypeConversions.fromUint32(value));
|
|
|
|
return this;
|
|
}
|
|
|
|
public ConfigSetter setByte(final ConfigArg arg, final byte value) {
|
|
checkArg(arg, ConfigType.BYTE);
|
|
|
|
putArgument(arg, new byte[]{value});
|
|
|
|
return this;
|
|
}
|
|
|
|
public ConfigSetter setByteList(final ConfigArg arg, final byte[] values) {
|
|
checkArg(arg, ConfigType.BYTE_LIST);
|
|
|
|
putArgument(arg, ArrayUtils.addAll(new byte[]{(byte) values.length}, values));
|
|
|
|
return this;
|
|
}
|
|
|
|
public ConfigSetter setHourMinute(final ConfigArg arg, final Date date) {
|
|
checkArg(arg, ConfigType.DATETIME_HH_MM);
|
|
|
|
final Calendar calendar = GregorianCalendar.getInstance();
|
|
calendar.setTime(date);
|
|
|
|
putArgument(arg, new byte[]{
|
|
(byte) calendar.get(Calendar.HOUR_OF_DAY),
|
|
(byte) calendar.get(Calendar.MINUTE)
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
private void putArgument(final ConfigArg arg, final byte[] encodedValue) {
|
|
final Map<ConfigArg, byte[]> groupMap;
|
|
if (arguments.containsKey(arg.getConfigGroup())) {
|
|
groupMap = arguments.get(arg.getConfigGroup());
|
|
} else {
|
|
groupMap = new LinkedHashMap<>();
|
|
arguments.put(arg.getConfigGroup(), groupMap);
|
|
}
|
|
|
|
groupMap.put(arg, encodedValue);
|
|
}
|
|
|
|
public byte[] encode(final ConfigGroup configGroup) {
|
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
|
|
final Map<ConfigArg, byte[]> configArgMap = arguments.get(configGroup);
|
|
|
|
try {
|
|
baos.write(CMD_SET);
|
|
baos.write(configGroup.getValue());
|
|
baos.write(configGroup.getVersion());
|
|
baos.write(0x00); // ?
|
|
baos.write(configArgMap.size());
|
|
for (final Map.Entry<ConfigArg, byte[]> arg : configArgMap.entrySet()) {
|
|
final ConfigType configType = arg.getKey().getConfigType(mGroupVersions);
|
|
baos.write(arg.getKey().getCode());
|
|
baos.write(configType.getValue());
|
|
baos.write(arg.getValue());
|
|
}
|
|
} catch (final IOException e) {
|
|
LOG.error("Failed to encode command", e);
|
|
}
|
|
|
|
return baos.toByteArray();
|
|
}
|
|
|
|
public void write(final TransactionBuilder builder) {
|
|
// Write one command per config group
|
|
for (final ConfigGroup configGroup : arguments.keySet()) {
|
|
ZeppOsConfigService.this.write(builder, encode(configGroup));
|
|
}
|
|
}
|
|
|
|
private void checkArg(final ConfigArg arg, final ConfigType expectedConfigType) {
|
|
if (arg.getConfigType(mGroupVersions) == null) {
|
|
// Some special cases (STEPS goal) do not have a config type
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (!expectedConfigType.equals(arg.getConfigType(mGroupVersions))) {
|
|
throw new IllegalArgumentException(
|
|
String.format(
|
|
"Invalid arg type %s for %s, expected %s",
|
|
expectedConfigType,
|
|
arg,
|
|
arg.getConfigType(mGroupVersions)
|
|
)
|
|
);
|
|
}
|
|
} catch (final IllegalArgumentException e) {
|
|
if (!BuildConfig.DEBUG) {
|
|
// Crash
|
|
throw e;
|
|
} else {
|
|
LOG.error(e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ConfigParser {
|
|
private final ConfigGroup configGroup;
|
|
private final boolean includesConstraints;
|
|
|
|
public ConfigParser(final ConfigGroup configGroup, final boolean includesConstraints) {
|
|
this.configGroup = configGroup;
|
|
this.includesConstraints = includesConstraints;
|
|
}
|
|
|
|
public Map<String, Object> parse(final int expectedNumConfigs, final byte[] bytes) {
|
|
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
|
final Map<String, Object> prefs = new HashMap<>();
|
|
|
|
int configCount = 0;
|
|
|
|
while (buf.position() < buf.limit()) {
|
|
if (configCount > expectedNumConfigs) {
|
|
LOG.error("Got more configs than {}", expectedNumConfigs);
|
|
return null;
|
|
}
|
|
|
|
final byte configArgByte = buf.get();
|
|
final ZeppOsConfigService.ConfigArg configArg = ZeppOsConfigService.ConfigArg.fromCode(configGroup, configArgByte);
|
|
if (configArg == null) {
|
|
LOG.error("Unknown config {} for {}", String.format("0x%02x", configArgByte), configGroup);
|
|
}
|
|
|
|
final byte configTypeByte = buf.get();
|
|
final ConfigType configType = ConfigType.fromValue(configTypeByte);
|
|
if (configType == null) {
|
|
LOG.error("Unknown type {} for {}", String.format("0x%02x", configTypeByte), configArg);
|
|
// Abort, since we don't know how to parse this type or how many bytes it is
|
|
// Return whatever we parsed so far, since that's still valid
|
|
return prefs;
|
|
}
|
|
if (configArg != null) {
|
|
if (configType != configArg.getConfigType(mGroupVersions)) {
|
|
LOG.warn("Unexpected arg type {} for {}, expected {}", configType, configArg, configArg.getConfigType(mGroupVersions));
|
|
}
|
|
}
|
|
|
|
Map<String, Object> argPrefs = null;
|
|
|
|
// FIXME this switch has a lot of repeated code that could be generalized...
|
|
switch (configType) {
|
|
case BOOL:
|
|
final ConfigBoolean valBoolean = ConfigBoolean.consume(buf);
|
|
if (valBoolean == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valBoolean);
|
|
if (configArg != null) {
|
|
argPrefs = convertBooleanToPrefs(configArg, valBoolean);
|
|
}
|
|
break;
|
|
case STRING:
|
|
final ConfigString valString = ConfigString.consume(buf, includesConstraints);
|
|
if (valString == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valString);
|
|
if (configArg != null) {
|
|
argPrefs = convertStringToPrefs(configArg, valString);
|
|
}
|
|
break;
|
|
case STRING_LIST:
|
|
final ConfigStringList valStringList = ConfigStringList.consume(buf, includesConstraints);
|
|
if (valStringList == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valStringList);
|
|
if (configArg != null) {
|
|
argPrefs = convertStringListToPrefs(configArg, valStringList);
|
|
}
|
|
break;
|
|
case SHORT:
|
|
final ConfigShort valShort = ConfigShort.consume(buf, includesConstraints);
|
|
if (valShort == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valShort);
|
|
if (configArg != null) {
|
|
argPrefs = convertShortToPrefs(configArg, valShort);
|
|
}
|
|
break;
|
|
case INT:
|
|
final ConfigInt valInt = ConfigInt.consume(buf, includesConstraints);
|
|
if (valInt == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valInt);
|
|
if (configArg != null) {
|
|
argPrefs = convertIntToPrefs(configArg, valInt);
|
|
}
|
|
break;
|
|
case BYTE:
|
|
final ConfigByte valByte = ConfigByte.consume(buf, includesConstraints);
|
|
if (valByte == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valByte);
|
|
if (configArg != null) {
|
|
argPrefs = convertByteToPrefs(configArg, valByte);
|
|
}
|
|
break;
|
|
case BYTE_LIST:
|
|
final ConfigByteList valByteList = ConfigByteList.consume(buf, includesConstraints);
|
|
if (valByteList == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valByteList);
|
|
if (configArg != null) {
|
|
argPrefs = convertByteListToPrefs(configArg, valByteList);
|
|
}
|
|
break;
|
|
case DATETIME_HH_MM:
|
|
final ConfigDatetimeHhMm valHhMm = ConfigDatetimeHhMm.consume(buf);
|
|
if (valHhMm == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valHhMm);
|
|
if (configArg != null) {
|
|
argPrefs = convertDatetimeHhMmToPrefs(configArg, valHhMm);
|
|
}
|
|
break;
|
|
case TIMESTAMP_MILLIS:
|
|
final ConfigTimestamp valTimestamp = ConfigTimestamp.consume(buf);
|
|
if (valTimestamp == null) {
|
|
LOG.error("Failed to parse {} for {}", configType, configArg);
|
|
return prefs;
|
|
}
|
|
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valTimestamp);
|
|
if (configArg != null) {
|
|
argPrefs = convertTimestampToPrefs(configArg, valTimestamp);
|
|
}
|
|
break;
|
|
default:
|
|
LOG.error("No parser for {}", configArg);
|
|
// Abort, since we don't know how to parse this type or how many bytes it is
|
|
// Return whatever we parsed so far, since that's still valid
|
|
return prefs;
|
|
}
|
|
|
|
if (argPrefs == null) {
|
|
LOG.warn("Unhandled {} pref of type {}", configType, configArg);
|
|
}
|
|
|
|
if (configArg != null && argPrefs != null && configType == configArg.getConfigType(mGroupVersions)) {
|
|
prefs.put(Huami2021Coordinator.getPrefKnownConfig(configArg.name()), true);
|
|
|
|
// 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 (Objects.equals(prefs.get(PREF_LANGUAGE), DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO)) {
|
|
argPrefs.remove(PREF_LANGUAGE);
|
|
}
|
|
}
|
|
if (argPrefs.containsKey(PREF_TIMEFORMAT) && prefs.containsKey(PREF_TIMEFORMAT)) {
|
|
if (Objects.equals(prefs.get(PREF_TIMEFORMAT), DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_AUTO)) {
|
|
argPrefs.remove(PREF_TIMEFORMAT);
|
|
}
|
|
}
|
|
|
|
prefs.putAll(argPrefs);
|
|
}
|
|
|
|
configCount++;
|
|
}
|
|
|
|
return prefs;
|
|
}
|
|
|
|
private Map<String, Object> convertBooleanToPrefs(final ConfigArg configArg, final ConfigBoolean value) {
|
|
// Special cases
|
|
switch (configArg) {
|
|
case LANGUAGE_FOLLOW_PHONE:
|
|
if (value.getValue()) {
|
|
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.getValue()) {
|
|
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;
|
|
}
|
|
|
|
if (configArg.getPrefKey() != null) {
|
|
// The arg maps to a boolean pref directly
|
|
return singletonMap(configArg.getPrefKey(), value.getValue());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Map<String, Object> convertStringToPrefs(final ConfigArg configArg, final ConfigString str) {
|
|
// Special cases
|
|
switch (configArg) {
|
|
case DATE_FORMAT:
|
|
return singletonMap(PREF_DATEFORMAT, str.getValue().replace(".", "/").toUpperCase(Locale.ROOT));
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (configArg.getPrefKey() != null) {
|
|
// The arg maps to a string pref directly
|
|
return singletonMap(configArg.getPrefKey(), str.getValue());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Map<String, Object> convertStringListToPrefs(final ConfigArg configArg, final ConfigStringList str) {
|
|
final List<String> possibleValues = str.getPossibleValues();
|
|
final boolean includesConstraints = !possibleValues.isEmpty();
|
|
Map<String, Object> prefs = null;
|
|
final ValueDecoder<String> decoder;
|
|
|
|
switch (configArg) {
|
|
case UPPER_BUTTON_LONG_PRESS:
|
|
case LOWER_BUTTON_PRESS:
|
|
decoder = Huami2021MenuType.displayItemNameLookup::get;
|
|
break;
|
|
default:
|
|
decoder = a -> a; // passthrough
|
|
}
|
|
|
|
if (configArg.getPrefKey() != null) {
|
|
prefs = singletonMap(configArg.getPrefKey(), decoder.decode(str.getValue()));
|
|
if (includesConstraints) {
|
|
prefs.put(
|
|
Huami2021Coordinator.getPrefPossibleValuesKey(configArg.getPrefKey()),
|
|
decodeStringValues(possibleValues, decoder)
|
|
);
|
|
}
|
|
}
|
|
|
|
return prefs;
|
|
}
|
|
|
|
private Map<String, Object> convertShortToPrefs(final ConfigArg configArg, final ConfigShort value) {
|
|
if (configArg.getPrefKey() != null) {
|
|
final Map<String, Object> prefs;
|
|
if (configArg == ConfigArg.CALL_DELAY) {
|
|
// Persist as string, otherwise the EditText crashes
|
|
prefs = singletonMap(configArg.getPrefKey(), String.valueOf(value.getValue()));
|
|
} else {
|
|
// The arg maps to a number pref directly
|
|
prefs = singletonMap(configArg.getPrefKey(), value.getValue());
|
|
}
|
|
|
|
if (value.isMinMaxKnown()) {
|
|
prefs.put(getPrefMinKey(configArg.getPrefKey()), value.getMin());
|
|
prefs.put(getPrefMaxKey(configArg.getPrefKey()), value.getMax());
|
|
}
|
|
|
|
return prefs;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Map<String, Object> convertIntToPrefs(final ConfigArg configArg, final ConfigInt value) {
|
|
if (configArg.getPrefKey() != null) {
|
|
// The arg maps to a number pref directly
|
|
final Map<String, Object> prefs = singletonMap(configArg.getPrefKey(), value.getValue());
|
|
|
|
if (value.isMinMaxKnown()) {
|
|
prefs.put(getPrefMinKey(configArg.getPrefKey()), value.getMin());
|
|
prefs.put(getPrefMaxKey(configArg.getPrefKey()), value.getMax());
|
|
}
|
|
|
|
return prefs;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Map<String, Object> convertTimestampToPrefs(final ConfigArg configArg, final ConfigTimestamp value) {
|
|
if (configArg.getPrefKey() != null) {
|
|
// The arg maps to a number pref directly
|
|
return singletonMap(configArg.getPrefKey(), value.getValue());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Map<String, Object> convertDatetimeHhMmToPrefs(final ConfigArg configArg, final ConfigDatetimeHhMm hhmm) {
|
|
if (configArg.getPrefKey() != null) {
|
|
// The arg maps to a hhmm pref directly
|
|
return singletonMap(configArg.getPrefKey(), hhmm.getValue());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Map<String, Object> convertByteListToPrefs(final ConfigArg configArg, final ConfigByteList value) {
|
|
final byte[] possibleValues = value.getPossibleValues();
|
|
final boolean includesConstraints = possibleValues != null && possibleValues.length > 0;
|
|
Map<String, Object> prefs = null;
|
|
final ValueDecoder<Byte> decoder;
|
|
|
|
switch (configArg) {
|
|
case WORKOUT_DETECTION_CATEGORY:
|
|
decoder = b -> decodeEnum(WORKOUT_DETECTION_CATEGORY_MAP, b);
|
|
break;
|
|
default:
|
|
LOG.warn("No decoder for {}", configArg);
|
|
return null;
|
|
}
|
|
|
|
if (configArg.getPrefKey() != null) {
|
|
final List<String> valuesList = decodeByteValues(value.getValues(), decoder);
|
|
prefs = singletonMap(configArg.getPrefKey(), new HashSet<>(valuesList));
|
|
if (includesConstraints) {
|
|
prefs.put(
|
|
Huami2021Coordinator.getPrefPossibleValuesKey(configArg.getPrefKey()),
|
|
String.join(",", decodeByteValues(possibleValues, decoder))
|
|
);
|
|
}
|
|
}
|
|
|
|
return prefs;
|
|
}
|
|
|
|
private Map<String, Object> convertByteToPrefs(final ConfigArg configArg, final ConfigByte value) {
|
|
final byte[] possibleValues = value.getPossibleValues();
|
|
final boolean includesConstraints = value.getPossibleValues().length > 0;
|
|
Map<String, Object> prefs = null;
|
|
final ValueDecoder<Byte> decoder;
|
|
|
|
switch (configArg) {
|
|
case ALWAYS_ON_DISPLAY_MODE:
|
|
decoder = b -> decodeEnum(ALWAYS_ON_DISPLAY_MAP, b);
|
|
break;
|
|
case LIFT_WRIST_MODE:
|
|
decoder = b -> decodeEnum(LIFT_WRIST_MAP, b);
|
|
break;
|
|
case LIFT_WRIST_RESPONSE_SENSITIVITY:
|
|
decoder = b -> decodeEnum(LIFT_WRIST_SENSITIVITY_MAP, b);
|
|
break;
|
|
case LANGUAGE:
|
|
final String language = languageByteToLocale(value.getValue());
|
|
if (language != null) {
|
|
prefs = singletonMap(configArg.getPrefKey(), language);
|
|
if (includesConstraints) {
|
|
final List<String> possibleLanguages = new ArrayList<>();
|
|
possibleLanguages.add("auto");
|
|
for (final byte possibleValue : value.getPossibleValues()) {
|
|
possibleLanguages.add(languageByteToLocale(possibleValue));
|
|
}
|
|
possibleLanguages.removeAll(Collections.singleton(null));
|
|
prefs.put(Huami2021Coordinator.getPrefPossibleValuesKey(configArg.getPrefKey()), String.join(",", possibleLanguages));
|
|
}
|
|
}
|
|
decoder = null;
|
|
break;
|
|
case HEART_RATE_ALL_DAY_MONITORING:
|
|
decoder = ZeppOsConfigService::decodeHeartRateAllDayMonitoring;
|
|
break;
|
|
case SCREEN_TIMEOUT:
|
|
case HEART_RATE_HIGH_ALERTS:
|
|
case HEART_RATE_LOW_ALERTS:
|
|
case SPO2_LOW_ALERT:
|
|
decoder = a -> String.format(Locale.ROOT, "%d", a & 0xff);
|
|
break;
|
|
case TIME_FORMAT:
|
|
decoder = b -> decodeString(TIME_FORMAT_MAP, b);
|
|
break;
|
|
case DND_MODE:
|
|
decoder = b -> decodeEnum(DND_MODE_MAP, b);
|
|
break;
|
|
case TEMPERATURE_UNIT:
|
|
// TODO: This should be per device...
|
|
decoder = b -> decodeEnum(TEMPERATURE_UNIT_MAP, b);
|
|
break;
|
|
case NIGHT_MODE_MODE:
|
|
decoder = b -> decodeString(NIGHT_MODE_MAP, b);
|
|
break;
|
|
case WEARING_DIRECTION_BUTTONS:
|
|
decoder = b -> decodeString(WEARING_DIRECTION_MAP, b);
|
|
break;
|
|
case OFFLINE_VOICE_LANGUAGE:
|
|
decoder = b -> decodeString(OFFLINE_VOICE_LANGUAGE_MAP, b);
|
|
break;
|
|
case WORKOUT_GPS_PRESET:
|
|
decoder = b -> decodeEnum(GPS_PRESET_MAP, b);
|
|
break;
|
|
case WORKOUT_GPS_BAND:
|
|
decoder = b -> decodeEnum(GPS_BAND_MAP, b);
|
|
break;
|
|
case WORKOUT_GPS_COMBINATION:
|
|
decoder = b -> decodeEnum(GPS_COMBINATION_MAP, b);
|
|
break;
|
|
case WORKOUT_GPS_SATELLITE_SEARCH:
|
|
decoder = b -> decodeEnum(GPS_SATELLITE_SEARCH_MAP, b);
|
|
break;
|
|
case WORKOUT_DETECTION_SENSITIVITY:
|
|
decoder = b -> decodeEnum(WORKOUT_DETECTION_SENSITIVITY_MAP, b);
|
|
break;
|
|
default:
|
|
decoder = null;
|
|
}
|
|
|
|
if (decoder != null) {
|
|
prefs = singletonMap(configArg.getPrefKey(), decoder.decode(value.getValue()));
|
|
if (includesConstraints) {
|
|
prefs.put(
|
|
Huami2021Coordinator.getPrefPossibleValuesKey(configArg.getPrefKey()),
|
|
String.join(",", decodeByteValues(possibleValues, decoder))
|
|
);
|
|
}
|
|
}
|
|
|
|
return prefs;
|
|
}
|
|
|
|
private List<String> decodeByteValues(final byte[] values, final ValueDecoder<Byte> decoder) {
|
|
final List<String> decoded = new ArrayList<>(values.length);
|
|
for (final byte b : values) {
|
|
final String decodedByte = decoder.decode(b);
|
|
if (decodedByte != null) {
|
|
decoded.add(decodedByte);
|
|
} else {
|
|
decoded.add(String.format("0x%x", b));
|
|
}
|
|
}
|
|
decoded.removeAll(Collections.singleton(null));
|
|
return decoded;
|
|
}
|
|
|
|
private String decodeStringValues(final List<String> values, final ValueDecoder<String> decoder) {
|
|
final List<String> decoded = new ArrayList<>(values.size());
|
|
for (final String str : values) {
|
|
final String decodedStr = decoder.decode(str);
|
|
if (decodedStr != null) {
|
|
decoded.add(decodedStr);
|
|
} else {
|
|
decoded.add(str);
|
|
}
|
|
}
|
|
if (decoded.isEmpty()) {
|
|
return null;
|
|
}
|
|
return String.join(",", decoded);
|
|
}
|
|
|
|
private Map<String, Object> singletonMap(final String key, final Object value) {
|
|
if (key == null) {
|
|
LOG.error("Null key in prefs update");
|
|
if (BuildConfig.DEBUG) {
|
|
// Crash
|
|
throw new IllegalStateException("Null key in prefs update");
|
|
}
|
|
return Collections.emptyMap();
|
|
}
|
|
|
|
return new HashMap<String, Object>() {{
|
|
put(key, value);
|
|
}};
|
|
}
|
|
}
|
|
|
|
private static class ConfigBoolean {
|
|
private final boolean value;
|
|
|
|
public ConfigBoolean(final boolean value) {
|
|
this.value = value;
|
|
}
|
|
|
|
public boolean getValue() {
|
|
return value;
|
|
}
|
|
|
|
private static ConfigBoolean consume(final ByteBuffer buf) {
|
|
return new ConfigBoolean(buf.get() == 1);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("ConfigBoolean{value=%s}", value);
|
|
}
|
|
}
|
|
|
|
private static class ConfigString {
|
|
private final String value;
|
|
private final int maxLength;
|
|
|
|
public ConfigString(final String value, final int maxLength) {
|
|
this.value = value;
|
|
this.maxLength = maxLength;
|
|
}
|
|
|
|
public String getValue() {
|
|
return value;
|
|
}
|
|
|
|
public int getMaxLength() {
|
|
return maxLength;
|
|
}
|
|
|
|
private static ConfigString consume(final ByteBuffer buf, final boolean includesConstraints) {
|
|
final String value = StringUtils.untilNullTerminator(buf);
|
|
if (value == null) {
|
|
LOG.error("Null terminator not found in buffer");
|
|
return null;
|
|
}
|
|
|
|
if (!includesConstraints) {
|
|
return new ConfigString(value, -1);
|
|
}
|
|
|
|
final int maxLength = buf.get() & 0xff;
|
|
|
|
return new ConfigString(value, maxLength);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("ConfigString{value=%s}", value);
|
|
}
|
|
}
|
|
|
|
private static class ConfigStringList {
|
|
private final String value;
|
|
private final List<String> possibleValues;
|
|
|
|
public ConfigStringList(final String value, final List<String> possibleValues) {
|
|
this.value = value;
|
|
this.possibleValues = possibleValues;
|
|
}
|
|
|
|
public String getValue() {
|
|
return value;
|
|
}
|
|
|
|
public List<String> getPossibleValues() {
|
|
return possibleValues;
|
|
}
|
|
|
|
private static ConfigStringList consume(final ByteBuffer buf, final boolean includesConstraints) {
|
|
final String value = StringUtils.untilNullTerminator(buf);
|
|
if (value == null) {
|
|
LOG.error("Null terminator not found in buffer");
|
|
return null;
|
|
}
|
|
|
|
final List<String> possibleValues = new ArrayList<>();
|
|
if (includesConstraints) {
|
|
final int unknown1 = buf.get() & 0xff; // ?
|
|
final int numPossibleValues = buf.get() & 0xff;
|
|
|
|
for (int i = 0; i < numPossibleValues; i++) {
|
|
final String possibleValue = StringUtils.untilNullTerminator(buf);
|
|
possibleValues.add(possibleValue);
|
|
}
|
|
}
|
|
|
|
return new ConfigStringList(value, possibleValues);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("ConfigStringList{value=%s, possibleValues=%s}", value, possibleValues);
|
|
}
|
|
}
|
|
|
|
private static class ConfigShort {
|
|
private final short value;
|
|
private final short min;
|
|
private final short max;
|
|
private final boolean minMaxKnown;
|
|
|
|
public ConfigShort(final short value) {
|
|
this.value = value;
|
|
this.min = this.max = 0;
|
|
minMaxKnown = false;
|
|
}
|
|
|
|
public ConfigShort(final short value, final short min, final short max) {
|
|
this.value = value;
|
|
this.min = min;
|
|
this.max = max;
|
|
this.minMaxKnown = true;
|
|
}
|
|
|
|
public short getValue() {
|
|
return value;
|
|
}
|
|
|
|
public short getMin() {
|
|
return min;
|
|
}
|
|
|
|
public short getMax() {
|
|
return max;
|
|
}
|
|
|
|
public boolean isMinMaxKnown() {
|
|
return minMaxKnown;
|
|
}
|
|
|
|
private static ConfigShort consume(final ByteBuffer buf, final boolean includesConstraints) {
|
|
final short value = buf.getShort();
|
|
|
|
if (!includesConstraints) {
|
|
return new ConfigShort(value);
|
|
}
|
|
|
|
final short min = buf.getShort();
|
|
final short max = buf.getShort();
|
|
|
|
return new ConfigShort(value, min, max);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
if (isMinMaxKnown()) {
|
|
return String.format(Locale.ROOT, "ConfigShort{value=%d, min=%d, max=%d}", value, min, max);
|
|
} else {
|
|
return String.format(Locale.ROOT, "ConfigShort{value=%d}", value);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class ConfigInt {
|
|
private final int value;
|
|
private final int min;
|
|
private final int max;
|
|
private final boolean minMaxKnown;
|
|
|
|
public ConfigInt(final int value) {
|
|
this.value = value;
|
|
this.min = this.max = 0;
|
|
minMaxKnown = false;
|
|
}
|
|
|
|
public ConfigInt(final int value, final int min, final int max) {
|
|
this.value = value;
|
|
this.min = min;
|
|
this.max = max;
|
|
this.minMaxKnown = true;
|
|
}
|
|
|
|
public int getValue() {
|
|
return value;
|
|
}
|
|
|
|
public int getMin() {
|
|
return min;
|
|
}
|
|
|
|
public int getMax() {
|
|
return max;
|
|
}
|
|
|
|
public boolean isMinMaxKnown() {
|
|
return minMaxKnown;
|
|
}
|
|
|
|
private static ConfigInt consume(final ByteBuffer buf, final boolean includesConstraints) {
|
|
final int value = buf.getInt();
|
|
|
|
if (!includesConstraints) {
|
|
return new ConfigInt(value);
|
|
}
|
|
|
|
final int min = buf.getInt();
|
|
final int max = buf.getInt();
|
|
|
|
return new ConfigInt(value, min, max);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
if (isMinMaxKnown()) {
|
|
return String.format(Locale.ROOT, "ConfigInt{value=%d, min=%d, max=%d}", value, min, max);
|
|
} else {
|
|
return String.format(Locale.ROOT, "ConfigInt{value=%d}", value);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class ConfigTimestamp {
|
|
private final long value;
|
|
|
|
public ConfigTimestamp(final long value) {
|
|
this.value = value;
|
|
}
|
|
|
|
public long getValue() {
|
|
return value;
|
|
}
|
|
|
|
private static ConfigTimestamp consume(final ByteBuffer buf) {
|
|
final long value = buf.getLong();
|
|
|
|
return new ConfigTimestamp(value);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format(Locale.ROOT, "ConfigTimestamp{value=%s}", new Date(value));
|
|
}
|
|
}
|
|
|
|
private static class ConfigByte {
|
|
private final byte value;
|
|
private final byte[] possibleValues;
|
|
|
|
public ConfigByte(final byte value, final byte[] possibleValues) {
|
|
this.value = value;
|
|
this.possibleValues = possibleValues;
|
|
}
|
|
|
|
public byte getValue() {
|
|
return value;
|
|
}
|
|
|
|
public byte[] getPossibleValues() {
|
|
return possibleValues;
|
|
}
|
|
|
|
private static ConfigByte consume(final ByteBuffer buf, final boolean includesConstraints) {
|
|
final byte value = buf.get();
|
|
|
|
if (includesConstraints) {
|
|
final int numPossibleValues = buf.get() & 0xff;
|
|
final byte[] possibleValues = new byte[numPossibleValues];
|
|
|
|
for (int i = 0; i < numPossibleValues; i++) {
|
|
possibleValues[i] = buf.get();
|
|
}
|
|
|
|
return new ConfigByte(value, possibleValues);
|
|
}
|
|
|
|
return new ConfigByte(value, new byte[0]);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("ConfigByte{value=0x%02x, possibleValues=%s}", value, GB.hexdump(possibleValues));
|
|
}
|
|
}
|
|
|
|
private static class ConfigByteList {
|
|
private final byte[] values;
|
|
private final byte[] possibleValues;
|
|
|
|
public ConfigByteList(final byte[] values, final byte[] possibleValues) {
|
|
this.values = values;
|
|
this.possibleValues = possibleValues;
|
|
}
|
|
|
|
public byte[] getValues() {
|
|
return values;
|
|
}
|
|
|
|
@Nullable
|
|
public byte[] getPossibleValues() {
|
|
return possibleValues;
|
|
}
|
|
|
|
private static ConfigByteList consume(final ByteBuffer buf, final boolean includesConstraints) {
|
|
final int numValues = buf.get() & 0xff;
|
|
final byte[] values = new byte[numValues];
|
|
for (int i = 0; i < numValues; i++) {
|
|
values[i] = buf.get();
|
|
}
|
|
|
|
if (includesConstraints) {
|
|
final int numPossibleValues = buf.get() & 0xff;
|
|
final byte[] possibleValues = new byte[numPossibleValues];
|
|
|
|
for (int i = 0; i < numPossibleValues; i++) {
|
|
possibleValues[i] = buf.get();
|
|
}
|
|
|
|
return new ConfigByteList(values, possibleValues);
|
|
}
|
|
|
|
return new ConfigByteList(values, null);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
if (possibleValues != null) {
|
|
return String.format("ConfigByteList{values=%s, possibleValues=%s}", GB.hexdump(values), GB.hexdump(possibleValues));
|
|
} else {
|
|
return String.format("ConfigByteList{values=%s}", GB.hexdump(values));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class ConfigDatetimeHhMm {
|
|
final String value;
|
|
|
|
public ConfigDatetimeHhMm(final String value) {
|
|
this.value = value;
|
|
}
|
|
|
|
public String getValue() {
|
|
return value;
|
|
}
|
|
|
|
private static ConfigDatetimeHhMm consume(final ByteBuffer buf) {
|
|
final DateFormat df = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
|
final String hhmm = String.format(Locale.ROOT, "%02d:%02d", buf.get(), buf.get());
|
|
try {
|
|
df.parse(hhmm);
|
|
} catch (final ParseException e) {
|
|
LOG.error("Failed to parse HH:mm from {}", hhmm);
|
|
return null;
|
|
}
|
|
return new ConfigDatetimeHhMm(hhmm);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("ConfigDatetimeHhMm{value=%s}", value);
|
|
}
|
|
}
|
|
|
|
private interface ValueDecoder<T> {
|
|
String decode(T val);
|
|
}
|
|
|
|
public static String languageByteToLocale(final byte code) {
|
|
final Map<Integer, String> localeLookup = MapUtils.reverse(HuamiLanguageType.idLookup);
|
|
return localeLookup.get((int) code);
|
|
}
|
|
|
|
public static Byte languageLocaleToByte(final String locale) {
|
|
if (HuamiLanguageType.idLookup.containsKey(locale)) {
|
|
return (byte) (int) HuamiLanguageType.idLookup.get(locale);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static String decodeHeartRateAllDayMonitoring(final byte b) {
|
|
if (b > 0) {
|
|
return String.format(Locale.ROOT, "%d", (b & 0xff) * 60);
|
|
} else {
|
|
return String.format(Locale.ROOT, "%d", b);
|
|
}
|
|
}
|
|
|
|
public static byte encodeHeartRateAllDayMonitoring(final String val) {
|
|
final int intVal = Integer.parseInt(val);
|
|
if (intVal < 0) {
|
|
return (byte) intVal;
|
|
} else {
|
|
return (byte) (intVal / 60);
|
|
}
|
|
}
|
|
|
|
private static final Map<Byte, Enum<?>> ALWAYS_ON_DISPLAY_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, AlwaysOnDisplay.OFF);
|
|
put((byte) 0x01, AlwaysOnDisplay.AUTOMATIC);
|
|
put((byte) 0x02, AlwaysOnDisplay.SCHEDULED);
|
|
put((byte) 0x03, AlwaysOnDisplay.ALWAYS);
|
|
}};
|
|
|
|
private static final Map<Byte, String> NIGHT_MODE_MAP = new HashMap<Byte, String>() {{
|
|
put((byte) 0x00, MiBandConst.PREF_NIGHT_MODE_OFF);
|
|
put((byte) 0x01, MiBandConst.PREF_NIGHT_MODE_SUNSET);
|
|
put((byte) 0x02, MiBandConst.PREF_NIGHT_MODE_SCHEDULED);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> DND_MODE_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, DoNotDisturb.OFF);
|
|
put((byte) 0x01, DoNotDisturb.SCHEDULED);
|
|
put((byte) 0x02, DoNotDisturb.AUTOMATIC);
|
|
put((byte) 0x03, DoNotDisturb.ALWAYS);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> TEMPERATURE_UNIT_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, MiBandConst.DistanceUnit.METRIC);
|
|
put((byte) 0x01, MiBandConst.DistanceUnit.IMPERIAL);
|
|
}};
|
|
|
|
private static final Map<Byte, String> TIME_FORMAT_MAP = new HashMap<Byte, String>() {{
|
|
put((byte) 0x00, DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H);
|
|
put((byte) 0x01, DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> LIFT_WRIST_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, ActivateDisplayOnLift.OFF);
|
|
put((byte) 0x01, ActivateDisplayOnLift.SCHEDULED);
|
|
put((byte) 0x02, ActivateDisplayOnLift.ON);
|
|
put((byte) 0x03, ActivateDisplayOnLift.SMART);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> LIFT_WRIST_SENSITIVITY_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, ActivateDisplayOnLiftSensitivity.NORMAL);
|
|
put((byte) 0x01, ActivateDisplayOnLiftSensitivity.SENSITIVE);
|
|
}};
|
|
|
|
private static final Map<Byte, String> WEARING_DIRECTION_MAP = new HashMap<Byte, String>() {{
|
|
put((byte) 0x00, "buttons_on_left");
|
|
put((byte) 0x01, "buttons_on_right");
|
|
}};
|
|
|
|
private static final Map<Byte, String> OFFLINE_VOICE_LANGUAGE_MAP = new HashMap<Byte, String>() {{
|
|
put((byte) 0x01, "zh_CN"); // TODO confirm
|
|
put((byte) 0x02, "en_US");
|
|
put((byte) 0x03, "de_DE");
|
|
put((byte) 0x04, "es_ES");
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> GPS_PRESET_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, GpsCapability.Preset.ACCURACY);
|
|
put((byte) 0x01, GpsCapability.Preset.BALANCED);
|
|
put((byte) 0x02, GpsCapability.Preset.POWER_SAVING);
|
|
put((byte) 0x04, GpsCapability.Preset.CUSTOM);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> GPS_BAND_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, GpsCapability.Band.SINGLE_BAND);
|
|
put((byte) 0x01, GpsCapability.Band.DUAL_BAND);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> GPS_COMBINATION_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, GpsCapability.Combination.LOW_POWER_GPS);
|
|
put((byte) 0x01, GpsCapability.Combination.GPS);
|
|
put((byte) 0x02, GpsCapability.Combination.GPS_BDS);
|
|
put((byte) 0x03, GpsCapability.Combination.GPS_GNOLASS);
|
|
put((byte) 0x04, GpsCapability.Combination.GPS_GALILEO);
|
|
put((byte) 0x05, GpsCapability.Combination.ALL_SATELLITES);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> GPS_SATELLITE_SEARCH_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, GpsCapability.SatelliteSearch.SPEED_FIRST);
|
|
put((byte) 0x01, GpsCapability.SatelliteSearch.ACCURACY_FIRST);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> WORKOUT_DETECTION_CATEGORY_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x03, WorkoutDetectionCapability.Category.WALKING);
|
|
put((byte) 0x28, WorkoutDetectionCapability.Category.INDOOR_WALKING);
|
|
put((byte) 0x01, WorkoutDetectionCapability.Category.OUTDOOR_RUNNING);
|
|
put((byte) 0x02, WorkoutDetectionCapability.Category.TREADMILL);
|
|
put((byte) 0x04, WorkoutDetectionCapability.Category.OUTDOOR_CYCLING);
|
|
put((byte) 0x06, WorkoutDetectionCapability.Category.POOL_SWIMMING);
|
|
put((byte) 0x09, WorkoutDetectionCapability.Category.ELLIPTICAL);
|
|
put((byte) 0x17, WorkoutDetectionCapability.Category.ROWING_MACHINE);
|
|
}};
|
|
|
|
private static final Map<Byte, Enum<?>> WORKOUT_DETECTION_SENSITIVITY_MAP = new HashMap<Byte, Enum<?>>() {{
|
|
put((byte) 0x00, WorkoutDetectionCapability.Sensitivity.HIGH);
|
|
put((byte) 0x01, WorkoutDetectionCapability.Sensitivity.STANDARD);
|
|
put((byte) 0x02, WorkoutDetectionCapability.Sensitivity.LOW);
|
|
}};
|
|
|
|
public static String decodeEnum(final Map<Byte, Enum<?>> map, final byte b) {
|
|
if (map.containsKey(b)) {
|
|
return map.get(b).name().toLowerCase(Locale.ROOT);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static String decodeString(final Map<Byte, String> map, final byte b) {
|
|
return map.get(b);
|
|
}
|
|
|
|
public static Byte encodeEnum(final Map<Byte, Enum<?>> map, final String val) {
|
|
final Map<Enum<?>, Byte> reverse = MapUtils.reverse(map);
|
|
for (final Enum<?> anEnum : reverse.keySet()) {
|
|
if (anEnum.name().toLowerCase(Locale.ROOT).equals(val)) {
|
|
return reverse.get(anEnum);
|
|
}
|
|
}
|
|
|
|
// Byte doesn't match a known enum value, attempt to parse it as hex
|
|
final Matcher matcher = Pattern.compile("^0[xX]([0-9a-fA-F]{1,2})$").matcher(val);
|
|
if (matcher.find()) {
|
|
return (byte) Integer.parseInt(matcher.group(1), 16);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static Byte encodeString(final Map<Byte, String> map, final String val) {
|
|
final Map<String, Byte> reverse = MapUtils.reverse(map);
|
|
for (final String aString : reverse.keySet()) {
|
|
if (aString.equals(val)) {
|
|
return reverse.get(aString);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|