992 lines
43 KiB
Java
992 lines
43 KiB
Java
/* Copyright (C) 2023-2024 Andreas Shimokawa, José Rebelo, LuK1337,
|
|
Yoran Vulker
|
|
|
|
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 <https://www.gnu.org/licenses/>. */
|
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
|
|
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.text.TextUtils;
|
|
|
|
import com.google.protobuf.InvalidProtocolBufferException;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Calendar;
|
|
import java.util.Collections;
|
|
import java.util.GregorianCalendar;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.TimeZone;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepStateDetection;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventWearState;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiFWHelper;
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
|
|
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiPreferences;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.SilentMode;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
|
|
|
public class XiaomiSystemService extends AbstractXiaomiService implements XiaomiDataUploadService.Callback {
|
|
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSystemService.class);
|
|
|
|
// We persist the settings code when receiving the display items,
|
|
// so we can enforce it when sending them
|
|
private static final String PREF_SETTINGS_DISPLAY_ITEM_CODE = "xiaomi_settings_display_item_code";
|
|
private static final int BATTERY_STATE_REQUEST_INTERVAL = (int) TimeUnit.MINUTES.toMillis(15);
|
|
|
|
public static final int COMMAND_TYPE = 2;
|
|
|
|
public static final int CMD_BATTERY = 1;
|
|
public static final int CMD_DEVICE_INFO = 2;
|
|
public static final int CMD_CLOCK = 3;
|
|
public static final int CMD_FIRMWARE_INSTALL = 5;
|
|
public static final int CMD_LANGUAGE = 6;
|
|
public static final int CMD_CAMERA_REMOTE_GET = 7;
|
|
public static final int CMD_CAMERA_REMOTE_SET = 8;
|
|
public static final int CMD_PASSWORD_GET = 9;
|
|
public static final int CMD_MISC_SETTING_GET = 14;
|
|
public static final int CMD_MISC_SETTING_SET = 15;
|
|
public static final int CMD_FIND_PHONE = 17;
|
|
public static final int CMD_FIND_WATCH = 18;
|
|
public static final int CMD_PASSWORD_SET = 21;
|
|
public static final int CMD_DISPLAY_ITEMS_GET = 29;
|
|
public static final int CMD_DISPLAY_ITEMS_SET = 30;
|
|
public static final int CMD_WORKOUT_TYPES_GET = 39;
|
|
public static final int CMD_MISC_SETTING_SET_FROM_BAND = 42;
|
|
public static final int CMD_SILENT_MODE_GET = 43;
|
|
public static final int CMD_SILENT_MODE_SET_FROM_PHONE = 44;
|
|
public static final int CMD_SILENT_MODE_SET_FROM_WATCH = 45;
|
|
public static final int CMD_WIDGET_SCREENS_GET = 51;
|
|
public static final int CMD_WIDGET_SCREENS_SET = 52;
|
|
public static final int CMD_WIDGET_PARTS_GET = 53;
|
|
public static final int CMD_DEVICE_STATE_GET = 78;
|
|
public static final int CMD_DEVICE_STATE = 79;
|
|
|
|
// Not null if we're installing a firmware
|
|
private XiaomiFWHelper fwHelper = null;
|
|
private Handler handler = new Handler(Looper.getMainLooper());
|
|
private final Runnable batteryStateRequestRunnable = () -> {
|
|
getSupport().sendCommand("get device status", COMMAND_TYPE, CMD_DEVICE_STATE_GET);
|
|
getSupport().sendCommand("get battery state", COMMAND_TYPE, CMD_BATTERY);
|
|
};
|
|
|
|
private WearingState currentWearingState = WearingState.UNKNOWN;
|
|
private BatteryState currentBatteryState = BatteryState.UNKNOWN;
|
|
private SleepState currentSleepDetectionState = SleepState.UNKNOWN;
|
|
|
|
public XiaomiSystemService(final XiaomiSupport support) {
|
|
super(support);
|
|
}
|
|
|
|
@Override
|
|
public void initialize() {
|
|
// Request device info and configs
|
|
getSupport().sendCommand("get device info", COMMAND_TYPE, CMD_DEVICE_INFO);
|
|
getSupport().sendCommand("get device status", COMMAND_TYPE, CMD_DEVICE_STATE_GET);
|
|
// device status request may initialize wearing, charger, sleeping, and activity state, so
|
|
// get battery level as a failsafe for devices that don't support CMD_DEVICE_STATE_SET command
|
|
getSupport().sendCommand("get battery state", COMMAND_TYPE, CMD_BATTERY);
|
|
getSupport().sendCommand("get password", COMMAND_TYPE, CMD_PASSWORD_GET);
|
|
getSupport().sendCommand("get display items", COMMAND_TYPE, CMD_DISPLAY_ITEMS_GET);
|
|
getSupport().sendCommand("get camera remote", COMMAND_TYPE, CMD_CAMERA_REMOTE_GET);
|
|
getSupport().sendCommand("get widgets", COMMAND_TYPE, CMD_WIDGET_SCREENS_GET);
|
|
getSupport().sendCommand("get widget parts", COMMAND_TYPE, CMD_WIDGET_PARTS_GET);
|
|
getSupport().sendCommand("get workout types", COMMAND_TYPE, CMD_WORKOUT_TYPES_GET);
|
|
|
|
rearmBatteryStateRequestTimer();
|
|
}
|
|
|
|
@Override
|
|
public void handleCommand(final XiaomiProto.Command cmd) {
|
|
switch (cmd.getSubtype()) {
|
|
case CMD_DEVICE_INFO:
|
|
handleDeviceInfo(cmd.getSystem().getDeviceInfo());
|
|
return;
|
|
case CMD_BATTERY:
|
|
handleBattery(cmd.getSystem().getPower().getBattery());
|
|
return;
|
|
case CMD_FIRMWARE_INSTALL:
|
|
final int installStatus = cmd.getSystem().getFirmwareInstallResponse().getStatus();
|
|
if (installStatus != 0) {
|
|
LOG.warn("Invalid firmware install status {} for {}", installStatus, fwHelper.getId());
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Firmware install status 0, uploading");
|
|
setDeviceBusy();
|
|
getSupport().getDataUploadService().setCallback(this);
|
|
getSupport().getDataUploadService().requestUpload(XiaomiDataUploadService.TYPE_FIRMWARE, fwHelper.getBytes());
|
|
return;
|
|
case CMD_PASSWORD_GET:
|
|
handlePassword(cmd.getSystem().getPassword());
|
|
return;
|
|
case CMD_MISC_SETTING_SET:
|
|
LOG.debug("Got misc setting set ack, status={}", cmd.getStatus());
|
|
return;
|
|
case CMD_CAMERA_REMOTE_GET:
|
|
handleCameraRemote(cmd.getSystem().getCamera());
|
|
return;
|
|
case CMD_CAMERA_REMOTE_SET:
|
|
LOG.debug("Got camera remote set ack, status={}", cmd.getStatus());
|
|
return;
|
|
case CMD_FIND_PHONE:
|
|
LOG.debug("Got find phone: {}", cmd.getSystem().getFindDevice());
|
|
if (cmd.hasSystem()) {
|
|
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
|
if (cmd.getSystem().getFindDevice() == 0) {
|
|
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
|
|
} else {
|
|
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
|
|
}
|
|
getSupport().evaluateGBDeviceEvent(findPhoneEvent);
|
|
}
|
|
return;
|
|
case CMD_DISPLAY_ITEMS_GET:
|
|
handleDisplayItems(cmd.getSystem().getDisplayItems());
|
|
return;
|
|
case CMD_WORKOUT_TYPES_GET:
|
|
handleWorkoutTypes(cmd.getSystem().getWorkoutTypes());
|
|
return;
|
|
case CMD_MISC_SETTING_SET_FROM_BAND:
|
|
handleMiscSettingSet(cmd.getSystem().getMiscSettingSet());
|
|
return;
|
|
case CMD_SILENT_MODE_GET:
|
|
handlePhoneSilentModeGet();
|
|
return;
|
|
case CMD_SILENT_MODE_SET_FROM_WATCH:
|
|
handlePhoneSilentModeSet(cmd.getSystem().getPhoneSilentModeSet());
|
|
return;
|
|
case CMD_WIDGET_SCREENS_GET:
|
|
handleWidgetScreens(cmd.getSystem().getWidgetScreens());
|
|
return;
|
|
case CMD_WIDGET_SCREENS_SET:
|
|
LOG.debug("Got widget screens set ack, status={}", cmd.getStatus());
|
|
return;
|
|
case CMD_WIDGET_PARTS_GET:
|
|
handleWidgetParts(cmd.getSystem().getWidgetParts());
|
|
return;
|
|
case CMD_DEVICE_STATE_GET:
|
|
handleBasicDeviceState(cmd.getSystem().hasBasicDeviceState()
|
|
? cmd.getSystem().getBasicDeviceState()
|
|
: null);
|
|
return;
|
|
case CMD_DEVICE_STATE:
|
|
handleDeviceState(cmd.getSystem().hasDeviceState()
|
|
? cmd.getSystem().getDeviceState()
|
|
: null);
|
|
return;
|
|
}
|
|
|
|
LOG.warn("Unknown system command {}", cmd.getSubtype());
|
|
}
|
|
|
|
@Override
|
|
public boolean onSendConfiguration(final String config, final Prefs prefs) {
|
|
switch (config) {
|
|
case DeviceSettingsPreferenceConst.PREF_WEARMODE:
|
|
setWearMode();
|
|
return true;
|
|
case DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE:
|
|
setCameraRemoteConfig();
|
|
return true;
|
|
case DeviceSettingsPreferenceConst.PREF_LANGUAGE:
|
|
setLanguage();
|
|
return true;
|
|
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT:
|
|
setCurrentTime();
|
|
return true;
|
|
case PasswordCapabilityImpl.PREF_PASSWORD_ENABLED:
|
|
case PasswordCapabilityImpl.PREF_PASSWORD:
|
|
setPassword();
|
|
return true;
|
|
case HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE:
|
|
setDisplayItems();
|
|
return true;
|
|
case DeviceSettingsPreferenceConst.PREF_WIDGETS:
|
|
setWidgets();
|
|
return true;
|
|
}
|
|
|
|
return super.onSendConfiguration(config, prefs);
|
|
}
|
|
|
|
public void setLanguage() {
|
|
String localeString = GBApplication.getDeviceSpecificSharedPrefs(getSupport().getDevice().getAddress()).getString(
|
|
DeviceSettingsPreferenceConst.PREF_LANGUAGE, DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO
|
|
);
|
|
if (DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO.equals(localeString)) {
|
|
String language = Locale.getDefault().getLanguage();
|
|
String country = Locale.getDefault().getCountry();
|
|
|
|
if (StringUtils.isNullOrEmpty(country)) {
|
|
// sometimes country is null, no idea why, guess it.
|
|
country = language;
|
|
}
|
|
localeString = language + "_" + country.toUpperCase();
|
|
}
|
|
|
|
LOG.info("Set language: {}", localeString);
|
|
|
|
getSupport().sendCommand(
|
|
"set language",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_LANGUAGE)
|
|
.setSystem(XiaomiProto.System.newBuilder().setLanguage(
|
|
XiaomiProto.Language.newBuilder().setCode(localeString.toLowerCase(Locale.ROOT))
|
|
))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
public void setCurrentTime() {
|
|
LOG.debug("Setting current time");
|
|
|
|
final Calendar now = GregorianCalendar.getInstance();
|
|
final TimeZone tz = TimeZone.getDefault();
|
|
|
|
final GBPrefs gbPrefs = new GBPrefs(new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getSupport().getDevice().getAddress())));
|
|
final String timeFormat = gbPrefs.getTimeFormat();
|
|
final boolean is24hour = DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H.equals(timeFormat);
|
|
|
|
final XiaomiProto.Clock clock = XiaomiProto.Clock.newBuilder()
|
|
.setTime(XiaomiProto.Time.newBuilder()
|
|
.setHour(now.get(Calendar.HOUR_OF_DAY))
|
|
.setMinute(now.get(Calendar.MINUTE))
|
|
.setSecond(now.get(Calendar.SECOND))
|
|
.setMillisecond(now.get(Calendar.MILLISECOND))
|
|
.build())
|
|
.setDate(XiaomiProto.Date.newBuilder()
|
|
.setYear(now.get(Calendar.YEAR))
|
|
.setMonth(now.get(Calendar.MONTH) + 1)
|
|
.setDay(now.get(Calendar.DATE))
|
|
.build())
|
|
.setTimezone(XiaomiProto.TimeZone.newBuilder()
|
|
.setZoneOffset(((now.get(Calendar.ZONE_OFFSET) / 1000) / 60) / 15)
|
|
.setDstOffset(((now.get(Calendar.DST_OFFSET) / 1000) / 60) / 15)
|
|
.setName(tz.getID())
|
|
.build())
|
|
.setIsNot24Hour(!is24hour)
|
|
.build();
|
|
|
|
getSupport().sendCommand(
|
|
"set time",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_CLOCK)
|
|
.setSystem(XiaomiProto.System.newBuilder().setClock(clock).build())
|
|
.build()
|
|
);
|
|
}
|
|
|
|
private void handleDeviceInfo(final XiaomiProto.DeviceInfo deviceInfo) {
|
|
LOG.debug("Got device info: fw={} hw={} sn={}", deviceInfo.getFirmware(), deviceInfo.getModel(), deviceInfo.getSerialNumber());
|
|
|
|
final GBDeviceEventVersionInfo gbDeviceEventVersionInfo = new GBDeviceEventVersionInfo();
|
|
gbDeviceEventVersionInfo.fwVersion = deviceInfo.getFirmware();
|
|
//gbDeviceEventVersionInfo.fwVersion2 = "N/A";
|
|
gbDeviceEventVersionInfo.hwVersion = deviceInfo.getModel();
|
|
getSupport().evaluateGBDeviceEvent(gbDeviceEventVersionInfo);
|
|
|
|
final GBDeviceEventUpdateDeviceInfo gbDeviceEventUpdateDeviceInfo = new GBDeviceEventUpdateDeviceInfo("SERIAL: ", deviceInfo.getSerialNumber());
|
|
getSupport().evaluateGBDeviceEvent(gbDeviceEventUpdateDeviceInfo);
|
|
}
|
|
|
|
private BatteryState convertBatteryStateFromRawValue(int chargerState) {
|
|
switch (chargerState) {
|
|
case 1:
|
|
return BatteryState.BATTERY_CHARGING;
|
|
case 2:
|
|
case 3:
|
|
return BatteryState.BATTERY_NORMAL;
|
|
}
|
|
|
|
return BatteryState.UNKNOWN;
|
|
}
|
|
|
|
private void handleBattery(final XiaomiProto.Battery battery) {
|
|
LOG.debug("Got battery: {}", battery.getLevel());
|
|
|
|
final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
|
batteryInfo.batteryIndex = 0;
|
|
batteryInfo.level = battery.getLevel();
|
|
|
|
// currentBatteryState may already be set if the DeviceState message contained the field,
|
|
// but since some models report their charger state through this message, we will update it
|
|
// from here
|
|
if (battery.hasState()) {
|
|
currentBatteryState = convertBatteryStateFromRawValue(battery.getState());
|
|
|
|
if (currentBatteryState == BatteryState.UNKNOWN) {
|
|
LOG.warn("Unknown battery state {}", battery.getState());
|
|
}
|
|
}
|
|
|
|
batteryInfo.state = currentBatteryState;
|
|
getSupport().evaluateGBDeviceEvent(batteryInfo);
|
|
|
|
// reset battery level request timer
|
|
rearmBatteryStateRequestTimer();
|
|
}
|
|
|
|
private void setPassword() {
|
|
final Prefs prefs = getDevicePrefs();
|
|
|
|
final boolean passwordEnabled = prefs.getBoolean(PasswordCapabilityImpl.PREF_PASSWORD_ENABLED, false);
|
|
final String password = prefs.getString(PasswordCapabilityImpl.PREF_PASSWORD, null);
|
|
|
|
LOG.info("Setting password: {}, {}", passwordEnabled, password);
|
|
|
|
if (password == null || password.isEmpty()) {
|
|
LOG.warn("Invalid password: {}", password);
|
|
return;
|
|
}
|
|
|
|
final XiaomiProto.Password.Builder passwordBuilder = XiaomiProto.Password.newBuilder()
|
|
.setState(passwordEnabled ? 2 : 1)
|
|
.setPassword(password);
|
|
|
|
getSupport().sendCommand(
|
|
"set password",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_PASSWORD_SET)
|
|
.setSystem(XiaomiProto.System.newBuilder().setPassword(passwordBuilder).build())
|
|
.build()
|
|
);
|
|
}
|
|
|
|
private void handlePassword(final XiaomiProto.Password password) {
|
|
LOG.debug("Got device password");
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(
|
|
PasswordCapabilityImpl.PREF_PASSWORD_ENABLED,
|
|
password.getState() == 2
|
|
);
|
|
if (password.hasPassword()) {
|
|
eventUpdatePreferences.withPreference(
|
|
PasswordCapabilityImpl.PREF_PASSWORD,
|
|
password.getPassword()
|
|
);
|
|
}
|
|
eventUpdatePreferences.withPreference(
|
|
XiaomiPreferences.FEAT_PASSWORD,
|
|
true
|
|
);
|
|
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
}
|
|
|
|
private void handleMiscSettingSet(final XiaomiProto.MiscSettingSet miscSettingSet) {
|
|
LOG.debug("Got misc setting set");
|
|
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences();
|
|
|
|
if (miscSettingSet.hasWearingMode()) {
|
|
final String wearMode;
|
|
|
|
switch (miscSettingSet.getWearingMode().getMode()) {
|
|
case 0:
|
|
wearMode = "band";
|
|
break;
|
|
case 1:
|
|
wearMode = "pebble";
|
|
break;
|
|
case 2:
|
|
wearMode = "necklace";
|
|
break;
|
|
default:
|
|
wearMode = null;
|
|
LOG.warn("Unknown wear mode {}", miscSettingSet.getWearingMode().getMode());
|
|
break;
|
|
}
|
|
|
|
eventUpdatePreferences.withPreference(XiaomiPreferences.FEAT_WEAR_MODE, wearMode != null)
|
|
.withPreference(DeviceSettingsPreferenceConst.PREF_WEARMODE, wearMode);
|
|
}
|
|
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
}
|
|
|
|
private void setWearMode() {
|
|
final String wearMode = getDevicePrefs().getString(DeviceSettingsPreferenceConst.PREF_WEARMODE, "band");
|
|
|
|
LOG.debug("Set wear mode to {}", wearMode);
|
|
|
|
final int wearModeInt;
|
|
|
|
if ("band".equals(wearMode)) {
|
|
wearModeInt = 0;
|
|
} else if ("pebble".equals(wearMode)) {
|
|
wearModeInt = 1;
|
|
} else if ("necklace".equals(wearMode)) {
|
|
wearModeInt = 2;
|
|
} else {
|
|
LOG.warn("Unknown wear mode {}", wearMode);
|
|
return;
|
|
}
|
|
|
|
getSupport().sendCommand(
|
|
"set wear mode",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_MISC_SETTING_SET)
|
|
.setSystem(XiaomiProto.System.newBuilder().setMiscSettingSet(
|
|
XiaomiProto.MiscSettingSet.newBuilder().setWearingMode(
|
|
XiaomiProto.WearingMode.newBuilder().setMode(wearModeInt)
|
|
)
|
|
))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
private void handleCameraRemote(final XiaomiProto.Camera camera) {
|
|
LOG.debug("Got camera remote enabled={}", camera.getEnabled());
|
|
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
|
.withPreference(XiaomiPreferences.FEAT_CAMERA_REMOTE, true)
|
|
.withPreference(DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE, camera.getEnabled());
|
|
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
}
|
|
|
|
private void setCameraRemoteConfig() {
|
|
final boolean enabled = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE, false);
|
|
|
|
LOG.debug("Set camera remote enabled={}", enabled);
|
|
|
|
getSupport().sendCommand(
|
|
"set camera remote",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_CAMERA_REMOTE_SET)
|
|
.setSystem(XiaomiProto.System.newBuilder().setCamera(
|
|
XiaomiProto.Camera.newBuilder().setEnabled(enabled)
|
|
))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
private void setDisplayItems() {
|
|
final Prefs prefs = getDevicePrefs();
|
|
final List<String> allScreens = new ArrayList<>(prefs.getList(DeviceSettingsUtils.getPrefPossibleValuesKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), Collections.emptyList()));
|
|
final List<String> allLabels = new ArrayList<>(prefs.getList(DeviceSettingsUtils.getPrefPossibleValueLabelsKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), Collections.emptyList()));
|
|
final List<String> enabledScreens = new ArrayList<>(prefs.getList(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE, Collections.emptyList()));
|
|
final String settingsCode = prefs.getString(PREF_SETTINGS_DISPLAY_ITEM_CODE, null);
|
|
|
|
if (allScreens.isEmpty()) {
|
|
LOG.warn("No list of all screens");
|
|
return;
|
|
}
|
|
|
|
if (allScreens.size() != allLabels.size()) {
|
|
LOG.warn(
|
|
"Mismatched allScreens ({}) and allLabels ({}) sizes - this should never happen",
|
|
allScreens.size(),
|
|
allLabels.size()
|
|
);
|
|
return;
|
|
}
|
|
|
|
final Map<String, String> labelsMap = new HashMap<>();
|
|
for (int i = 0; i < allScreens.size(); i++) {
|
|
labelsMap.put(allScreens.get(i), allLabels.get(i));
|
|
}
|
|
|
|
LOG.debug("Setting display items: {}", enabledScreens);
|
|
|
|
if (settingsCode != null && !enabledScreens.contains(settingsCode)) {
|
|
enabledScreens.add(settingsCode);
|
|
}
|
|
|
|
boolean inMoreSection = false;
|
|
final XiaomiProto.DisplayItems.Builder displayItems = XiaomiProto.DisplayItems.newBuilder();
|
|
for (final String enabledScreen : enabledScreens) {
|
|
if (enabledScreen.equals("more")) {
|
|
inMoreSection = true;
|
|
continue;
|
|
}
|
|
if (labelsMap.get(enabledScreen) == null) {
|
|
continue;
|
|
}
|
|
|
|
final XiaomiProto.DisplayItem.Builder displayItem = XiaomiProto.DisplayItem.newBuilder()
|
|
.setCode(enabledScreen)
|
|
.setName(labelsMap.get(enabledScreen))
|
|
.setUnknown5(1);
|
|
|
|
if (inMoreSection) {
|
|
displayItem.setInMoreSection(true);
|
|
}
|
|
|
|
if (enabledScreen.equals(settingsCode)) {
|
|
displayItem.setIsSettings(1);
|
|
}
|
|
|
|
displayItems.addDisplayItem(displayItem);
|
|
}
|
|
|
|
for (final String screen : allScreens) {
|
|
if (enabledScreens.contains(screen)) {
|
|
continue;
|
|
}
|
|
|
|
final XiaomiProto.DisplayItem.Builder displayItem = XiaomiProto.DisplayItem.newBuilder()
|
|
.setCode(screen)
|
|
.setName(labelsMap.get(screen))
|
|
.setDisabled(true)
|
|
.setUnknown5(1);
|
|
|
|
displayItems.addDisplayItem(displayItem);
|
|
}
|
|
|
|
getSupport().sendCommand(
|
|
"set display items",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_DISPLAY_ITEMS_SET)
|
|
.setSystem(XiaomiProto.System.newBuilder().setDisplayItems(displayItems))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
private void handleDisplayItems(final XiaomiProto.DisplayItems displayItems) {
|
|
LOG.debug("Got {} display items", displayItems.getDisplayItemCount());
|
|
|
|
final List<String> allScreens = new ArrayList<>();
|
|
final List<String> allScreensLabels = new ArrayList<>();
|
|
final List<String> mainScreens = new ArrayList<>();
|
|
final List<String> moreScreens = new ArrayList<>();
|
|
String settingsCode = null;
|
|
for (final XiaomiProto.DisplayItem displayItem : displayItems.getDisplayItemList()) {
|
|
allScreens.add(displayItem.getCode());
|
|
allScreensLabels.add(displayItem.getName().replace(",", ""));
|
|
if (!displayItem.getDisabled()) {
|
|
if (displayItem.getInMoreSection()) {
|
|
moreScreens.add(displayItem.getCode());
|
|
} else {
|
|
mainScreens.add(displayItem.getCode());
|
|
}
|
|
}
|
|
|
|
if (displayItem.getIsSettings() == 1) {
|
|
settingsCode = displayItem.getCode();
|
|
}
|
|
}
|
|
|
|
final List<String> enabledScreens = new ArrayList<>(mainScreens);
|
|
if (!moreScreens.isEmpty()) {
|
|
enabledScreens.add("more");
|
|
enabledScreens.addAll(moreScreens);
|
|
}
|
|
|
|
allScreens.add("more");
|
|
allScreensLabels.add(getSupport().getContext().getString(R.string.menuitem_more));
|
|
|
|
final String allScreensPrefValue = StringUtils.join(",", allScreens.toArray(new String[0])).toString();
|
|
final String allScreensLabelsPrefValue = StringUtils.join(",", allScreensLabels.toArray(new String[0])).toString();
|
|
final String prefValue = StringUtils.join(",", enabledScreens.toArray(new String[0])).toString();
|
|
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
|
.withPreference(XiaomiPreferences.FEAT_DISPLAY_ITEMS, displayItems.getDisplayItemCount() > 0)
|
|
.withPreference(DeviceSettingsUtils.getPrefPossibleValuesKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), allScreensPrefValue)
|
|
.withPreference(DeviceSettingsUtils.getPrefPossibleValueLabelsKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), allScreensLabelsPrefValue)
|
|
.withPreference(PREF_SETTINGS_DISPLAY_ITEM_CODE, settingsCode)
|
|
.withPreference(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE, prefValue);
|
|
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
}
|
|
|
|
private void handleWorkoutTypes(final XiaomiProto.WorkoutTypes workoutTypes) {
|
|
LOG.debug("Got {} workout types", workoutTypes.getWorkoutTypeCount());
|
|
|
|
final List<String> codes = new ArrayList<>(workoutTypes.getWorkoutTypeCount());
|
|
for (final XiaomiProto.WorkoutType workoutType : workoutTypes.getWorkoutTypeList()) {
|
|
codes.add(String.valueOf(workoutType.getType()));
|
|
}
|
|
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
|
.withPreference(XiaomiPreferences.PREF_WORKOUT_TYPES, TextUtils.join(",", codes));
|
|
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
}
|
|
|
|
private void handleWearingState(int newStateValue) {
|
|
WearingState newState;
|
|
|
|
switch (newStateValue) {
|
|
case 1:
|
|
newState = WearingState.WEARING;
|
|
break;
|
|
case 2:
|
|
newState = WearingState.NOT_WEARING;
|
|
break;
|
|
default:
|
|
LOG.warn("Unknown wearing state {}", newStateValue);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Current wearing state = {}, new wearing state = {}", currentWearingState, newState);
|
|
|
|
if (currentWearingState != WearingState.UNKNOWN && currentWearingState != newState) {
|
|
GBDeviceEventWearState event = new GBDeviceEventWearState();
|
|
event.wearingState = newState;
|
|
getSupport().evaluateGBDeviceEvent(event);
|
|
}
|
|
|
|
currentWearingState = newState;
|
|
}
|
|
|
|
private void handleSleepDetectionState(int newStateValue) {
|
|
SleepState newState;
|
|
|
|
switch (newStateValue) {
|
|
case 1:
|
|
newState = SleepState.ASLEEP;
|
|
break;
|
|
case 2:
|
|
newState = SleepState.AWAKE;
|
|
break;
|
|
default:
|
|
LOG.warn("Unknown sleep detection state {}", newStateValue);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Current sleep detection state = {}, new sleep detection state = {}", currentSleepDetectionState, newState);
|
|
|
|
if (currentSleepDetectionState != SleepState.UNKNOWN && currentSleepDetectionState != newState) {
|
|
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection();
|
|
event.sleepState = newState;
|
|
getSupport().evaluateGBDeviceEvent(event);
|
|
}
|
|
|
|
currentSleepDetectionState = newState;
|
|
}
|
|
|
|
public void handlePhoneSilentModeGet() {
|
|
LOG.debug("Watch requested phone silent mode");
|
|
sendPhoneSilentMode(SilentMode.isPhoneInSilenceMode(getSupport().getDevice().getAddress()));
|
|
}
|
|
|
|
public void handlePhoneSilentModeSet(final XiaomiProto.PhoneSilentModeSet phoneSilentModeSet) {
|
|
final boolean silent = phoneSilentModeSet.getPhoneSilentMode().getSilent();
|
|
|
|
LOG.debug("Set phone silent mode = {}", silent);
|
|
SilentMode.setPhoneSilentMode(getSupport().getDevice().getAddress(), silent);
|
|
}
|
|
|
|
private void sendPhoneSilentMode(final boolean enabled) {
|
|
getSupport().sendCommand(
|
|
"send phone silent mode = " + enabled,
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_SILENT_MODE_SET_FROM_PHONE)
|
|
.setSystem(XiaomiProto.System.newBuilder().setPhoneSilentModeSet(
|
|
XiaomiProto.PhoneSilentModeSet.newBuilder().setPhoneSilentMode(
|
|
XiaomiProto.PhoneSilentMode.newBuilder().setSilent(enabled)
|
|
)
|
|
))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
public void handleBasicDeviceState(XiaomiProto.BasicDeviceState deviceState) {
|
|
LOG.debug("Got basic device state: {}", deviceState);
|
|
|
|
if (null == deviceState) {
|
|
LOG.warn("Got null for BasicDeviceState, requesting battery state and returning");
|
|
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
|
|
return;
|
|
}
|
|
|
|
// If we got basic device state, we support device actions
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(
|
|
XiaomiPreferences.FEAT_DEVICE_ACTIONS,
|
|
true
|
|
);
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
|
|
// handle battery info from message
|
|
{
|
|
BatteryState newBatteryState = deviceState.getIsCharging() ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
|
|
LOG.debug("Previous charging state: {}, new charging state: {}", currentBatteryState, newBatteryState);
|
|
|
|
currentBatteryState = newBatteryState;
|
|
|
|
// if the device state did not have a battery level, request it from the device through other means.
|
|
// the battery state is now cached, so that it can be used when another response with battery level is received.
|
|
if (!deviceState.hasBatteryLevel()) {
|
|
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
|
|
} else {
|
|
GBDeviceEventBatteryInfo event = new GBDeviceEventBatteryInfo();
|
|
event.batteryIndex = 0;
|
|
event.state = newBatteryState;
|
|
event.level = deviceState.getBatteryLevel();
|
|
getSupport().evaluateGBDeviceEvent(event);
|
|
}
|
|
}
|
|
|
|
// handle sleep state from message
|
|
{
|
|
SleepState newSleepState = deviceState.getIsUserAsleep() ? SleepState.ASLEEP : SleepState.AWAKE;
|
|
LOG.debug("Previous sleep state: {}, new sleep state: {}", currentSleepDetectionState, newSleepState);
|
|
|
|
// send event if the previous state is known and the new state is different from cached
|
|
if (currentSleepDetectionState != SleepState.UNKNOWN && currentSleepDetectionState != newSleepState) {
|
|
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection();
|
|
event.sleepState = newSleepState;
|
|
getSupport().evaluateGBDeviceEvent(event);
|
|
}
|
|
|
|
currentSleepDetectionState = newSleepState;
|
|
}
|
|
|
|
// handle wearing state from message
|
|
{
|
|
WearingState newWearingState = deviceState.getIsWorn() ? WearingState.WEARING : WearingState.NOT_WEARING;
|
|
LOG.debug("Previous wearing state: {}, new wearing state: {}", currentWearingState, newWearingState);
|
|
|
|
if (currentWearingState != WearingState.UNKNOWN && currentWearingState != newWearingState) {
|
|
GBDeviceEventWearState event = new GBDeviceEventWearState();
|
|
event.wearingState = newWearingState;
|
|
getSupport().evaluateGBDeviceEvent(event);
|
|
}
|
|
|
|
currentWearingState = newWearingState;
|
|
}
|
|
|
|
// TODO: handle activity state
|
|
|
|
// reset battery level refresh timer
|
|
rearmBatteryStateRequestTimer();
|
|
}
|
|
|
|
public void handleDeviceState(XiaomiProto.DeviceState deviceState) {
|
|
LOG.debug("Got device state: {}", deviceState);
|
|
|
|
if (null == deviceState) {
|
|
LOG.warn("Got null for DeviceState, requesting battery state and returning");
|
|
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
|
|
return;
|
|
}
|
|
|
|
if (deviceState.hasWearingState()) {
|
|
handleWearingState(deviceState.getWearingState());
|
|
}
|
|
|
|
// The charger state of some devices can only be known when listening for device status
|
|
// updates. If available, this state will be cached here and updated in the GBDevice upon
|
|
// the next retrieval of the battery level
|
|
if (deviceState.hasChargingState()) {
|
|
BatteryState newBatteryState = convertBatteryStateFromRawValue(deviceState.getChargingState());
|
|
|
|
LOG.debug("Current battery state = {}, new battery state = {}", currentBatteryState, newBatteryState);
|
|
|
|
if (currentBatteryState != newBatteryState) {
|
|
currentBatteryState = newBatteryState;
|
|
}
|
|
}
|
|
|
|
if (deviceState.hasSleepState()) {
|
|
handleSleepDetectionState(deviceState.getSleepState());
|
|
}
|
|
|
|
// TODO process warning (unknown possible values) and activity information
|
|
|
|
// request battery state to request battery level and charger state on supported models
|
|
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
|
|
}
|
|
|
|
private void handleWidgetScreens(final XiaomiProto.WidgetScreens widgetScreens) {
|
|
LOG.debug("Got {} widget screens", widgetScreens.getWidgetScreenCount());
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
|
// FIXME we're just persisting the protobuf bytes - probably not a good idea
|
|
.withPreference(XiaomiPreferences.PREF_WIDGET_SCREENS, GB.hexdump(widgetScreens.toByteArray()));
|
|
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
}
|
|
|
|
private void handleWidgetParts(final XiaomiProto.WidgetParts widgetParts) {
|
|
LOG.debug("Got {} widget parts", widgetParts.getWidgetPartCount());
|
|
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
|
.withPreference(XiaomiPreferences.FEAT_WIDGETS, widgetParts.getWidgetPartCount() > 0)
|
|
// FIXME we're just persisting the protobuf bytes - probably not a good idea
|
|
.withPreference(XiaomiPreferences.PREF_WIDGET_PARTS, GB.hexdump(widgetParts.toByteArray()));
|
|
|
|
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
|
}
|
|
|
|
private void setWidgets() {
|
|
// Just take the persisted protobuf and send it (see above and XiaomiWidgetManager)
|
|
final String hex = getDevicePrefs().getString(XiaomiPreferences.PREF_WIDGET_SCREENS, null);
|
|
if (hex == null) {
|
|
LOG.warn("raw widget screens hex is null");
|
|
return;
|
|
}
|
|
|
|
final XiaomiProto.WidgetScreens widgetScreens;
|
|
try {
|
|
widgetScreens = XiaomiProto.WidgetScreens.parseFrom(GB.hexStringToByteArray(hex));
|
|
} catch (final InvalidProtocolBufferException e) {
|
|
LOG.warn("failed to parse raw widget screns hex");
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Setting {} widget screens", widgetScreens.getWidgetScreenCount());
|
|
|
|
getSupport().sendCommand(
|
|
"set widgets",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_WIDGET_SCREENS_SET)
|
|
.setSystem(XiaomiProto.System.newBuilder().setWidgetScreens(widgetScreens))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
public void onFindPhone(final boolean start) {
|
|
LOG.debug("Find phone: {}", start);
|
|
|
|
if (!start) {
|
|
// Stop on watch
|
|
getSupport().sendCommand(
|
|
"find phone stop",
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_FIND_PHONE)
|
|
.setSystem(XiaomiProto.System.newBuilder().setFindDevice(1).build())
|
|
.build()
|
|
);
|
|
}
|
|
}
|
|
|
|
public void onFindWatch(final boolean start) {
|
|
LOG.debug("Find watch: {}", start);
|
|
|
|
getSupport().sendCommand(
|
|
"find watch " + start,
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_FIND_WATCH)
|
|
.setSystem(XiaomiProto.System.newBuilder().setFindDevice(start ? 0 : 1).build())
|
|
.build()
|
|
);
|
|
}
|
|
|
|
public void installFirmware(final XiaomiFWHelper fwHelper) {
|
|
assert fwHelper.isValid();
|
|
assert fwHelper.isFirmware();
|
|
|
|
this.fwHelper = fwHelper;
|
|
|
|
getSupport().sendCommand(
|
|
"install firmware " + fwHelper.getVersion(),
|
|
XiaomiProto.Command.newBuilder()
|
|
.setType(COMMAND_TYPE)
|
|
.setSubtype(CMD_FIRMWARE_INSTALL)
|
|
.setSystem(XiaomiProto.System.newBuilder().setFirmwareInstallRequest(
|
|
XiaomiProto.FirmwareInstallRequest.newBuilder()
|
|
.setUnknown1(0)
|
|
.setUnknown2(0)
|
|
.setVersion(fwHelper.getVersion())
|
|
.setMd5(GB.hexdump(CheckSums.md5(fwHelper.getBytes())).toLowerCase(Locale.ROOT))
|
|
))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
private void setDeviceBusy() {
|
|
final GBDevice device = getSupport().getDevice();
|
|
device.setBusyTask(getSupport().getContext().getString(R.string.updating_firmware));
|
|
device.sendDeviceUpdateIntent(getSupport().getContext());
|
|
}
|
|
|
|
private void unsetDeviceBusy() {
|
|
final GBDevice device = getSupport().getDevice();
|
|
if (device != null && device.isConnected()) {
|
|
if (device.isBusy()) {
|
|
device.unsetBusyTask();
|
|
device.sendDeviceUpdateIntent(getSupport().getContext());
|
|
}
|
|
device.sendDeviceUpdateIntent(getSupport().getContext());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onUploadFinish(final boolean success) {
|
|
LOG.debug("Firmware upload finished: {}", success);
|
|
|
|
if (getSupport().getConnectionSpecificSupport() != null) {
|
|
getSupport().getConnectionSpecificSupport().runOnQueue("firmware upload finish", () -> {
|
|
getSupport().getDataUploadService().setCallback(null);
|
|
|
|
final int notificationMessage = success ?
|
|
R.string.updatefirmwareoperation_update_complete :
|
|
R.string.updatefirmwareoperation_write_failed;
|
|
|
|
onUploadProgress(notificationMessage, 100, false);
|
|
unsetDeviceBusy();
|
|
|
|
fwHelper = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onUploadProgress(final int progressPercent) {
|
|
onUploadProgress(R.string.updatefirmwareoperation_update_in_progress, progressPercent, true);
|
|
}
|
|
|
|
public void onUploadProgress(final int stringResource, final int progressPercent, final boolean ongoing) {
|
|
getSupport().getConnectionSpecificSupport().onUploadProgress(stringResource, progressPercent, ongoing);
|
|
}
|
|
|
|
private void rearmBatteryStateRequestTimer() {
|
|
this.handler.removeCallbacks(this.batteryStateRequestRunnable);
|
|
this.handler.postDelayed(this.batteryStateRequestRunnable, BATTERY_STATE_REQUEST_INTERVAL);
|
|
}
|
|
|
|
@Override
|
|
public void onDisconnect() {
|
|
this.handler.removeCallbacks(this.batteryStateRequestRunnable);
|
|
}
|
|
}
|