/* 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 . */ 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 allScreens = new ArrayList<>(prefs.getList(DeviceSettingsUtils.getPrefPossibleValuesKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), Collections.emptyList())); final List allLabels = new ArrayList<>(prefs.getList(DeviceSettingsUtils.getPrefPossibleValueLabelsKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), Collections.emptyList())); final List 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 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 allScreens = new ArrayList<>(); final List allScreensLabels = new ArrayList<>(); final List mainScreens = new ArrayList<>(); final List 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 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 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); } }