mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Mi Band 8: Initial support (WIP)
This commit is contained in:
parent
fac566c7da
commit
fda3b53657
@ -292,6 +292,10 @@ dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.21.7'
|
||||
implementation 'com.android.volley:volley:1.2.1'
|
||||
|
||||
// TODO pull just the needed classes into GB?
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.71'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15to18:1.71'
|
||||
|
||||
// NON-FOSS dependencies
|
||||
// implementation('androidx.core:core-google-shortcuts:1.0.1') {
|
||||
// exclude group:'com.google.android.gms'
|
||||
|
@ -125,7 +125,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDevice(final GBDevice gbDevice) throws GBException {
|
||||
public final void deleteDevice(final GBDevice gbDevice) throws GBException {
|
||||
LOG.info("will try to delete device: " + gbDevice.getName());
|
||||
if (gbDevice.isConnected() || gbDevice.isConnecting()) {
|
||||
GBApplication.deviceService(gbDevice).disconnect();
|
||||
|
@ -0,0 +1,28 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
|
||||
public class XiaomiActivitySummaryParser implements ActivitySummaryParser {
|
||||
@Override
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
|
||||
// TODO parse it
|
||||
return summary;
|
||||
}
|
||||
}
|
@ -0,0 +1,438 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPairingActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
|
||||
public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
return super.createBLEScanFilters();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull final GBDevice gbDevice,
|
||||
@NonNull final Device device,
|
||||
@NonNull final DaoSession session) throws GBException {
|
||||
// TODO, and fix on zepp
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(final GBDevice device, DaoSession session) {
|
||||
return new XiaomiSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends StressSample> getStressSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO XiaomiStressSampleProvider
|
||||
return super.getStressSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO XiaomiSpo2SampleProvider
|
||||
return super.getSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends HeartRateSample> getHeartRateMaxSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO XiaomiHeartRateMaxSampleProvider
|
||||
return super.getHeartRateMaxSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends HeartRateSample> getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO XiaomiHeartRateRestingSampleProvider
|
||||
return super.getHeartRateRestingSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends HeartRateSample> getHeartRateManualSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO XiaomiHeartRateManualSampleProvider
|
||||
return super.getHeartRateManualSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends PaiSample> getPaiSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO XiaomiPaiSampleProvider
|
||||
return super.getPaiSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends SleepRespiratoryRateSample> getSleepRespiratoryRateSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
// TODO XiaomiSleepRespiratoryRateSampleProvider
|
||||
return super.getSleepRespiratoryRateSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
|
||||
return new XiaomiActivitySummaryParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFlashing() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount(final GBDevice device) {
|
||||
// TODO the watch returns the slot count
|
||||
return 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement(final GBDevice device) {
|
||||
// TODO maybe for watchfaces or widgets?
|
||||
return super.supportsAppsManagement(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_REQUIRE_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsStressMeasurement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateStats() {
|
||||
// TODO does it?
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPai() {
|
||||
// TODO does it?
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSleepRespiratoryRate() {
|
||||
// TODO does it?
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmSnoozing() {
|
||||
// TODO does it?
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmDescription(final GBDevice device) {
|
||||
// TODO does it?
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMusicInfo() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaximumReminderMessageLength() {
|
||||
// TODO does it?
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReminderSlotCount(final GBDevice device) {
|
||||
// TODO Does it?
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWorldClocksSlotCount() {
|
||||
// TODO how many?
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWorldClocksLabelLength() {
|
||||
// TODO how much?
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsDisabledWorldClocks() {
|
||||
// TODO does it?
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsManualHeartRateMeasurement(final GBDevice device) {
|
||||
// TODO orchestrate
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Xiaomi";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
// TODO supports steps?
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRemSleep() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUnicodeEmojis() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(final GBDevice device) {
|
||||
final List<Integer> settings = new ArrayList<>();
|
||||
|
||||
// TODO review this
|
||||
|
||||
//
|
||||
// Time
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_time);
|
||||
settings.add(R.xml.devicesettings_timeformat);
|
||||
settings.add(R.xml.devicesettings_dateformat_2);
|
||||
if (getWorldClocksSlotCount() > 0) {
|
||||
settings.add(R.xml.devicesettings_world_clocks);
|
||||
}
|
||||
|
||||
//
|
||||
// Display
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_display);
|
||||
settings.add(R.xml.devicesettings_huami2021_displayitems);
|
||||
settings.add(R.xml.devicesettings_huami2021_shortcuts);
|
||||
settings.add(R.xml.devicesettings_nightmode);
|
||||
settings.add(R.xml.devicesettings_sleep_mode);
|
||||
settings.add(R.xml.devicesettings_liftwrist_display_sensitivity_with_smart);
|
||||
settings.add(R.xml.devicesettings_password);
|
||||
settings.add(R.xml.devicesettings_always_on_display);
|
||||
settings.add(R.xml.devicesettings_screen_timeout);
|
||||
|
||||
//
|
||||
// Health
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_health);
|
||||
settings.add(R.xml.devicesettings_heartrate_sleep_alert_activity_stress_spo2);
|
||||
settings.add(R.xml.devicesettings_inactivity_dnd_no_threshold);
|
||||
settings.add(R.xml.devicesettings_goal_notification);
|
||||
|
||||
//
|
||||
// Workout
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_workout);
|
||||
settings.add(R.xml.devicesettings_workout_start_on_phone);
|
||||
settings.add(R.xml.devicesettings_workout_send_gps_to_band);
|
||||
|
||||
//
|
||||
// Notifications
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_notifications);
|
||||
settings.add(R.xml.devicesettings_display_caller);
|
||||
settings.add(R.xml.devicesettings_vibrationpatterns);
|
||||
settings.add(R.xml.devicesettings_donotdisturb_withauto_and_always);
|
||||
settings.add(R.xml.devicesettings_screen_on_on_notifications);
|
||||
settings.add(R.xml.devicesettings_autoremove_notifications);
|
||||
settings.add(R.xml.devicesettings_canned_reply_16);
|
||||
|
||||
//
|
||||
// Calendar
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_calendar);
|
||||
settings.add(R.xml.devicesettings_sync_calendar);
|
||||
|
||||
//
|
||||
// Other
|
||||
//
|
||||
settings.add(R.xml.devicesettings_header_other);
|
||||
settings.add(R.xml.devicesettings_camera_remote);
|
||||
|
||||
return ArrayUtils.toPrimitive(settings.toArray(new Integer[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
|
||||
return new int[]{R.xml.devicesettings_pairingkey};
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
|
||||
return new XiaomiSettingsCustomizer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedLanguageSettings(final GBDevice device) {
|
||||
// TODO check which are supported
|
||||
final List<String> allLanguages = new ArrayList<>(HuamiLanguageType.idLookup.keySet());
|
||||
allLanguages.add(0, "auto");
|
||||
return allLanguages.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return MiBandPairingActivity.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PasswordCapabilityImpl.Mode getPasswordCapability() {
|
||||
return PasswordCapabilityImpl.Mode.NUMBERS_6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HeartRateCapability.MeasurementInterval> getHeartRateMeasurementIntervals() {
|
||||
return Arrays.asList(
|
||||
HeartRateCapability.MeasurementInterval.OFF,
|
||||
HeartRateCapability.MeasurementInterval.SMART,
|
||||
HeartRateCapability.MeasurementInterval.MINUTES_1,
|
||||
HeartRateCapability.MeasurementInterval.MINUTES_10,
|
||||
HeartRateCapability.MeasurementInterval.MINUTES_30
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsNotificationVibrationPatterns() {
|
||||
// TODO maybe can used this
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsNotificationVibrationRepetitionPatterns() {
|
||||
// TODO maybe can used this
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsNotificationLedPatterns() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractNotificationPattern[] getNotificationVibrationPatterns() {
|
||||
// TODO maybe can used this
|
||||
return new AbstractNotificationPattern[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractNotificationPattern[] getNotificationVibrationRepetitionPatterns() {
|
||||
// TODO maybe can used this
|
||||
return new AbstractNotificationPattern[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractNotificationPattern[] getNotificationLedPatterns() {
|
||||
return new AbstractNotificationPattern[0];
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return XiaomiSupport.class;
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
// TODO s/HuamiExtendedActivitySample/XiaomiActivitySample/g
|
||||
public class XiaomiSampleProvider extends AbstractSampleProvider<HuamiExtendedActivitySample> {
|
||||
protected XiaomiSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<HuamiExtendedActivitySample, ?> getSampleDao() {
|
||||
return getSession().getHuamiExtendedActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return HuamiExtendedActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return HuamiExtendedActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return HuamiExtendedActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(final int rawType) {
|
||||
// TODO
|
||||
return rawType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(final int activityKind) {
|
||||
return activityKind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(final int rawIntensity) {
|
||||
return rawIntensity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HuamiExtendedActivitySample createActivitySample() {
|
||||
return new HuamiExtendedActivitySample();
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class XiaomiSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
@Override
|
||||
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPreferenceKeysWithSummary() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
public static final Creator<XiaomiSettingsCustomizer> CREATOR = new Creator<XiaomiSettingsCustomizer>() {
|
||||
@Override
|
||||
public XiaomiSettingsCustomizer createFromParcel(final Parcel in) {
|
||||
return new XiaomiSettingsCustomizer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public XiaomiSettingsCustomizer[] newArray(final int size) {
|
||||
return new XiaomiSettingsCustomizer[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class MiBand8Coordinator extends XiaomiCoordinator {
|
||||
private final Pattern NAME_PATTTERN = Pattern.compile("^Xiaomi Smart Band 8 [A-Z0-9]{4}$");
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(final GBDeviceCandidate candidate) {
|
||||
if (NAME_PATTTERN.matcher(candidate.getName()).matches()) {
|
||||
return DeviceType.MIBAND8;
|
||||
}
|
||||
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
|
||||
// TODO implement this
|
||||
return super.findInstallHandler(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_miband8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_miband6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_miband6_disabled;
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ public class ActivityUser {
|
||||
private int activityUserCaloriesBurntGoal;
|
||||
private int activityUserDistanceGoalMeters;
|
||||
private int activityUserActiveTimeGoalMinutes;
|
||||
private int activityUserStandingTimeGoalHours;
|
||||
private int activityUserStepLengthCm;
|
||||
|
||||
private static final String defaultUserName = "gadgetbridge-user";
|
||||
@ -167,6 +168,7 @@ public class ActivityUser {
|
||||
activityUserCaloriesBurntGoal = prefs.getInt(PREF_USER_CALORIES_BURNT, defaultUserCaloriesBurntGoal);
|
||||
activityUserDistanceGoalMeters = prefs.getInt(PREF_USER_DISTANCE_METERS, defaultUserDistanceGoalMeters);
|
||||
activityUserActiveTimeGoalMinutes = prefs.getInt(PREF_USER_ACTIVETIME_MINUTES, defaultUserActiveTimeGoalMinutes);
|
||||
activityUserStandingTimeGoalHours = prefs.getInt(PREF_USER_GOAL_STANDING_TIME_HOURS, defaultUserGoalStandingTimeHours);
|
||||
activityUserStepLengthCm = prefs.getInt(PREF_USER_STEP_LENGTH_CM, defaultUserStepLengthCm);
|
||||
}
|
||||
|
||||
@ -199,4 +201,12 @@ public class ActivityUser {
|
||||
}
|
||||
return activityUserActiveTimeGoalMinutes;
|
||||
}
|
||||
|
||||
public int getStandingTimeGoalHours()
|
||||
{
|
||||
if (activityUserStandingTimeGoalHours < 1) {
|
||||
activityUserStandingTimeGoalHours = defaultUserGoalStandingTimeHours;
|
||||
}
|
||||
return activityUserStandingTimeGoalHours;
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ public interface Alarm extends Serializable {
|
||||
byte ALARM_SAT = 32;
|
||||
byte ALARM_SUN = 64;
|
||||
|
||||
byte ALARM_DAILY = Alarm.ALARM_MON | Alarm.ALARM_TUE | Alarm.ALARM_WED | Alarm.ALARM_THU | Alarm.ALARM_FRI | Alarm.ALARM_SAT | Alarm.ALARM_SUN;
|
||||
|
||||
int getPosition();
|
||||
|
||||
boolean getEnabled();
|
||||
|
@ -140,6 +140,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveHrCoordin
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.waspos.WaspOSCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8.MiBand8Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
|
||||
|
||||
@ -192,6 +193,7 @@ public enum DeviceType {
|
||||
AMAZFITPOP(AmazfitPopCoordinator.class),
|
||||
AMAZFITPOPPRO(AmazfitPopProCoordinator.class),
|
||||
MIBAND7(MiBand7Coordinator.class),
|
||||
MIBAND8(MiBand8Coordinator.class),
|
||||
AMAZFITGTS3(AmazfitGTS3Coordinator.class),
|
||||
AMAZFITGTR3(AmazfitGTR3Coordinator.class),
|
||||
AMAZFITGTR4(AmazfitGTR4Coordinator.class),
|
||||
|
@ -0,0 +1,307 @@
|
||||
/* Copyright (C) 2023 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.xiaomi;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiConstants.UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.crypto.CryptoException;
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.modes.CCMBlockCipher;
|
||||
import org.bouncycastle.crypto.params.AEADParameters;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class XiaomiCipher {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiCipher.class);
|
||||
|
||||
private final XiaomiSupport mSupport;
|
||||
|
||||
private final byte[] secretKey = new byte[16];
|
||||
private final byte[] nonce = new byte[16];
|
||||
private final byte[] encryptionKey = new byte[16];
|
||||
private final byte[] decryptionKey = new byte[16];
|
||||
private final byte[] encryptionNonce = new byte[4];
|
||||
private final byte[] decryptionNonce = new byte[4];
|
||||
|
||||
public XiaomiCipher(final XiaomiSupport support) {
|
||||
this.mSupport = support;
|
||||
}
|
||||
|
||||
protected void startAuthentication(final TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(mSupport.getDevice(), GBDevice.State.AUTHENTICATING, mSupport.getContext()));
|
||||
|
||||
System.arraycopy(getSecretKey(mSupport.getDevice()), 0, secretKey, 0, 16);
|
||||
new SecureRandom().nextBytes(nonce);
|
||||
|
||||
builder.write(
|
||||
mSupport.getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
|
||||
ArrayUtils.addAll(XiaomiConstants.PAYLOAD_HEADER_AUTH, buildNonceCommand(nonce))
|
||||
);
|
||||
}
|
||||
|
||||
protected void handleAuthCommand(final XiaomiProto.Command cmd) {
|
||||
if (cmd.getType() != XiaomiConstants.CMD_TYPE_AUTH) {
|
||||
throw new IllegalArgumentException("Not an auth command");
|
||||
}
|
||||
|
||||
switch (cmd.getSubtype()) {
|
||||
case XiaomiConstants.CMD_AUTH_NONCE: {
|
||||
LOG.debug("Got watch nonce");
|
||||
|
||||
// Watch nonce
|
||||
final XiaomiProto.Command reply = handleWatchNonce(cmd.getAuth().getWatchNonce());
|
||||
if (reply == null) {
|
||||
mSupport.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("auth step 2");
|
||||
// TODO maybe move these writes to support class?
|
||||
builder.write(
|
||||
mSupport.getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
|
||||
ArrayUtils.addAll(XiaomiConstants.PAYLOAD_HEADER_AUTH, reply.toByteArray())
|
||||
);
|
||||
builder.queue(mSupport.getQueue());
|
||||
break;
|
||||
}
|
||||
|
||||
case XiaomiConstants.CMD_AUTH_AUTH: {
|
||||
LOG.info("Authenticated!");
|
||||
|
||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("phase 2 initialize");
|
||||
builder.add(new SetDeviceStateAction(mSupport.getDevice(), GBDevice.State.INITIALIZED, mSupport.getContext()));
|
||||
mSupport.phase2Initialize(builder);
|
||||
builder.queue(mSupport.getQueue());
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOG.warn("Unknown auth payload subtype {}", cmd.getSubtype());
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encrypt(final byte[] arr, final short i) {
|
||||
final ByteBuffer packetNonce = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN)
|
||||
.put(encryptionNonce)
|
||||
.putInt(0)
|
||||
.putShort(i) // TODO what happens once this overflows?
|
||||
.putShort((short) 0);
|
||||
|
||||
try {
|
||||
return encrypt(encryptionKey, packetNonce.array(), arr);
|
||||
} catch (final CryptoException e) {
|
||||
throw new RuntimeException("failed to encrypt", e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(final byte[] arr) {
|
||||
final ByteBuffer packetNonce = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN);
|
||||
packetNonce.put(decryptionNonce);
|
||||
packetNonce.putInt(0);
|
||||
packetNonce.putInt(0);
|
||||
|
||||
try {
|
||||
return decrypt(decryptionKey, packetNonce.array(), arr);
|
||||
} catch (final CryptoException e) {
|
||||
throw new RuntimeException("failed to decrypt", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private XiaomiProto.Command handleWatchNonce(final XiaomiProto.WatchNonce watchNonce) {
|
||||
final byte[] step2hmac = computeAuthStep3Hmac(secretKey, nonce, watchNonce.getNonce().toByteArray());
|
||||
|
||||
System.arraycopy(step2hmac, 0, decryptionKey, 0, 16);
|
||||
System.arraycopy(step2hmac, 16, encryptionKey, 0, 16);
|
||||
System.arraycopy(step2hmac, 32, decryptionNonce, 0, 4);
|
||||
System.arraycopy(step2hmac, 36, encryptionNonce, 0, 4);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
LOG.debug("decryptionKey: {}", GB.hexdump(decryptionKey));
|
||||
LOG.debug("encryptionKey: {}", GB.hexdump(encryptionKey));
|
||||
LOG.debug("decryptionNonce: {}", GB.hexdump(decryptionNonce));
|
||||
LOG.debug("encryptionNonce: {}", GB.hexdump(encryptionNonce));
|
||||
}
|
||||
|
||||
final byte[] decryptionConfirmation = hmacSHA256(decryptionKey, ArrayUtils.addAll(watchNonce.getNonce().toByteArray(), nonce));
|
||||
if (!Arrays.equals(decryptionConfirmation, watchNonce.getHmac().toByteArray())) {
|
||||
LOG.warn("Watch hmac mismatch");
|
||||
return null;
|
||||
}
|
||||
|
||||
final XiaomiProto.AuthDeviceInfo authDeviceInfo = XiaomiProto.AuthDeviceInfo.newBuilder()
|
||||
.setUnknown1(0) // TODO ?
|
||||
.setPhoneApiLevel(Build.VERSION.SDK_INT)
|
||||
.setPhoneName(Build.MODEL)
|
||||
.setUnknown3(224) // TODO ?
|
||||
// TODO region should be actual device region?
|
||||
.setRegion(Locale.getDefault().getLanguage().substring(0, 2).toUpperCase(Locale.ROOT))
|
||||
.build();
|
||||
|
||||
final byte[] encryptedNonces = hmacSHA256(encryptionKey, ArrayUtils.addAll(nonce, watchNonce.getNonce().toByteArray()));
|
||||
final byte[] encryptedDeviceInfo = encrypt(authDeviceInfo.toByteArray(), (short) 0);
|
||||
final XiaomiProto.AuthStep3 authStep3 = XiaomiProto.AuthStep3.newBuilder()
|
||||
.setEncryptedNonces(ByteString.copyFrom(encryptedNonces))
|
||||
.setEncryptedDeviceInfo(ByteString.copyFrom(encryptedDeviceInfo))
|
||||
.build();
|
||||
|
||||
final XiaomiProto.Command.Builder cmd = XiaomiProto.Command.newBuilder();
|
||||
cmd.setType(XiaomiConstants.CMD_TYPE_AUTH);
|
||||
cmd.setSubtype(XiaomiConstants.CMD_AUTH_AUTH);
|
||||
|
||||
final XiaomiProto.Auth.Builder auth = XiaomiProto.Auth.newBuilder();
|
||||
auth.setAuthStep3(authStep3);
|
||||
|
||||
return cmd.setAuth(auth.build()).build();
|
||||
}
|
||||
|
||||
public static byte[] buildNonceCommand(final byte[] nonce) {
|
||||
final XiaomiProto.PhoneNonce.Builder phoneNonce = XiaomiProto.PhoneNonce.newBuilder();
|
||||
phoneNonce.setNonce(ByteString.copyFrom(nonce));
|
||||
|
||||
final XiaomiProto.Auth.Builder auth = XiaomiProto.Auth.newBuilder();
|
||||
auth.setPhoneNonce(phoneNonce.build());
|
||||
|
||||
final XiaomiProto.Command.Builder command = XiaomiProto.Command.newBuilder();
|
||||
command.setType(XiaomiConstants.CMD_TYPE_AUTH);
|
||||
command.setSubtype(XiaomiConstants.CMD_AUTH_NONCE);
|
||||
command.setAuth(auth.build());
|
||||
return command.build().toByteArray();
|
||||
}
|
||||
|
||||
public static byte[] computeAuthStep3Hmac(final byte[] secretKey,
|
||||
final byte[] phoneNonce,
|
||||
final byte[] watchNonce) {
|
||||
final byte[] miwearAuthBytes = "miwear-auth".getBytes();
|
||||
|
||||
final Mac mac;
|
||||
try {
|
||||
mac = Mac.getInstance("HmacSHA256");
|
||||
// Compute the actual key and re-initialize the mac
|
||||
mac.init(new SecretKeySpec(ArrayUtils.addAll(phoneNonce, watchNonce), "HmacSHA256"));
|
||||
final byte[] hmacKeyBytes = mac.doFinal(secretKey);
|
||||
final SecretKeySpec key = new SecretKeySpec(hmacKeyBytes, "HmacSHA256");
|
||||
mac.init(key);
|
||||
} catch (final NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new IllegalStateException("Failed to initialize hmac for auth step 2", e);
|
||||
}
|
||||
|
||||
final byte[] output = new byte[64];
|
||||
byte[] tmp = new byte[0];
|
||||
byte b = 1;
|
||||
int i = 0;
|
||||
while (i < output.length) {
|
||||
mac.update(tmp);
|
||||
mac.update(miwearAuthBytes);
|
||||
mac.update(b);
|
||||
tmp = mac.doFinal();
|
||||
for (int j = 0; j < tmp.length && i < output.length; j++, i++) {
|
||||
output[i] = tmp[j];
|
||||
}
|
||||
b++;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
protected static byte[] getSecretKey(final GBDevice device) {
|
||||
final byte[] authKeyBytes = new byte[16];
|
||||
|
||||
final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
|
||||
final String authKey = sharedPrefs.getString("authkey", null);
|
||||
if (StringUtils.isNotBlank(authKey)) {
|
||||
final byte[] srcBytes;
|
||||
// Allow both with and without 0x, to avoid user mistakes
|
||||
if (authKey.length() == 34 && authKey.startsWith("0x")) {
|
||||
srcBytes = GB.hexStringToByteArray(authKey.trim().substring(2));
|
||||
} else {
|
||||
srcBytes = GB.hexStringToByteArray(authKey.trim());
|
||||
}
|
||||
System.arraycopy(srcBytes, 0, authKeyBytes, 0, Math.min(srcBytes.length, 16));
|
||||
}
|
||||
|
||||
return authKeyBytes;
|
||||
}
|
||||
|
||||
protected static byte[] hmacSHA256(final byte[] key, final byte[] input) {
|
||||
try {
|
||||
final Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
return mac.doFinal(input);
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Failed to hmac", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] encrypt(final byte[] key, final byte[] nonce, final byte[] payload) throws
|
||||
CryptoException {
|
||||
final CCMBlockCipher cipher = createBlockCipher(true, new SecretKeySpec(key, "AES"), nonce);
|
||||
final byte[] out = new byte[cipher.getOutputSize(payload.length)];
|
||||
final int outBytes = cipher.processBytes(payload, 0, payload.length, out, 0);
|
||||
cipher.doFinal(out, outBytes);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static byte[] decrypt(final byte[] key,
|
||||
final byte[] nonce,
|
||||
final byte[] encryptedPayload) throws CryptoException {
|
||||
final CCMBlockCipher cipher = createBlockCipher(false, new SecretKeySpec(key, "AES"), nonce);
|
||||
final byte[] decrypted = new byte[cipher.getOutputSize(encryptedPayload.length)];
|
||||
cipher.doFinal(decrypted, cipher.processBytes(encryptedPayload, 0, encryptedPayload.length, decrypted, 0));
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
public static CCMBlockCipher createBlockCipher(final boolean forEncrypt,
|
||||
final SecretKey secretKey,
|
||||
final byte[] nonce) {
|
||||
final AESEngine aesFastEngine = new AESEngine();
|
||||
aesFastEngine.init(forEncrypt, new KeyParameter(secretKey.getEncoded()));
|
||||
final CCMBlockCipher blockCipher = new CCMBlockCipher(aesFastEngine);
|
||||
blockCipher.init(forEncrypt, new AEADParameters(new KeyParameter(secretKey.getEncoded()), 32, nonce, null));
|
||||
return blockCipher;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2023 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.xiaomi;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class XiaomiConstants {
|
||||
public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
public static final UUID UUID_SERVICE_XIAOMI_FE95 = UUID.fromString((String.format(BASE_UUID, "fe95")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0050 = UUID.fromString((String.format(BASE_UUID, "0050")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ = UUID.fromString((String.format(BASE_UUID, "0051")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE = UUID.fromString((String.format(BASE_UUID, "0052")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_ACTIVITY_DATA = UUID.fromString((String.format(BASE_UUID, "0053")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0054 = UUID.fromString((String.format(BASE_UUID, "0054")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_WATCHFACE = UUID.fromString((String.format(BASE_UUID, "0055")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0056 = UUID.fromString((String.format(BASE_UUID, "0056")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0057 = UUID.fromString((String.format(BASE_UUID, "0057")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0058 = UUID.fromString((String.format(BASE_UUID, "0058")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0059 = UUID.fromString((String.format(BASE_UUID, "0059")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_005A = UUID.fromString((String.format(BASE_UUID, "005a")));
|
||||
|
||||
public static final UUID UUID_SERVICE_XIAOMI_FDAB = UUID.fromString((String.format(BASE_UUID, "fdab")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0001 = UUID.fromString((String.format(BASE_UUID, "0001")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0002 = UUID.fromString((String.format(BASE_UUID, "0002")));
|
||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0003 = UUID.fromString((String.format(BASE_UUID, "0003")));
|
||||
|
||||
public static final int CMD_TYPE_AUTH = 1;
|
||||
public static final int CMD_TYPE_SYSTEM = 2;
|
||||
|
||||
public static final int CMD_AUTH_NONCE = 26;
|
||||
public static final int CMD_AUTH_AUTH = 27;
|
||||
|
||||
public static final int CMD_SYSTEM_BATTERY = 1;
|
||||
public static final int CMD_SYSTEM_DEVICE_INFO = 2;
|
||||
public static final int CMD_SYSTEM_CLOCK = 3;
|
||||
public static final int CMD_SYSTEM_CHARGER = 79;
|
||||
|
||||
// TODO not like this
|
||||
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
|
||||
public static final byte[] PAYLOAD_HEADER_AUTH = new byte[]{0, 0, 2, 2};
|
||||
}
|
@ -0,0 +1,565 @@
|
||||
/* Copyright (C) 2023 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.xiaomi;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiConstants.*;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiScheduleService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiSystemService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiWeatherService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class XiaomiSupport extends AbstractBTLEDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSupport.class);
|
||||
|
||||
private final XiaomiCipher cipher = new XiaomiCipher(this);
|
||||
|
||||
private final XiaomiMusicService musicService = new XiaomiMusicService(this);
|
||||
private final XiaomiHealthService healthService = new XiaomiHealthService(this);
|
||||
private final XiaomiNotificationService notificationService = new XiaomiNotificationService(this);
|
||||
private final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this);
|
||||
private final XiaomiWeatherService weatherService = new XiaomiWeatherService(this);
|
||||
private final XiaomiSystemService systemService = new XiaomiSystemService(this);
|
||||
|
||||
private final Map<Integer, AbstractXiaomiService> mServiceMap = new LinkedHashMap<Integer, AbstractXiaomiService>() {{
|
||||
put(XiaomiMusicService.COMMAND_TYPE, musicService);
|
||||
put(XiaomiHealthService.COMMAND_TYPE, healthService);
|
||||
put(XiaomiNotificationService.COMMAND_TYPE, notificationService);
|
||||
put(XiaomiScheduleService.COMMAND_TYPE, scheduleService);
|
||||
put(XiaomiWeatherService.COMMAND_TYPE, weatherService);
|
||||
put(XiaomiSystemService.COMMAND_TYPE, systemService);
|
||||
}};
|
||||
|
||||
public XiaomiSupport() {
|
||||
super(LOG);
|
||||
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
|
||||
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
|
||||
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
|
||||
addSupportedService(GattService.UUID_SERVICE_HUMAN_INTERFACE_DEVICE);
|
||||
addSupportedService(UUID_SERVICE_XIAOMI_FE95);
|
||||
addSupportedService(UUID_SERVICE_XIAOMI_FDAB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getImplicitCallbackModify() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(final GBDevice gbDevice, final BluetoothAdapter btAdapter, final Context context) {
|
||||
super.setContext(gbDevice, btAdapter, context);
|
||||
for (final AbstractXiaomiService service : mServiceMap.values()) {
|
||||
service.setContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
|
||||
final BluetoothGattCharacteristic characteristicCommandWrite = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE);
|
||||
final BluetoothGattCharacteristic characteristicCommandRead = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ);
|
||||
|
||||
if (characteristicCommandWrite == null || characteristicCommandRead == null) {
|
||||
LOG.warn("Command characteristics are null, will attempt to reconnect");
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT, getContext()));
|
||||
return builder;
|
||||
}
|
||||
|
||||
// FIXME why is this needed?
|
||||
getDevice().setFirmwareVersion("...");
|
||||
//getDevice().setFirmwareVersion2("...");
|
||||
|
||||
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ), true);
|
||||
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), true);
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
cipher.startAuthentication(builder);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
||||
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final UUID characteristicUUID = characteristic.getUuid();
|
||||
final byte[] value = characteristic.getValue();
|
||||
|
||||
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE.equals(characteristicUUID)) {
|
||||
if (Arrays.equals(value, PAYLOAD_ACK)) {
|
||||
LOG.debug("Got command write ack");
|
||||
} else {
|
||||
LOG.warn("Unexpected notification from command write: {}", GB.hexdump(value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ.equals(characteristicUUID)) {
|
||||
sendAck(characteristic);
|
||||
|
||||
final int header = BLETypeConversions.toUint16(value, 0);
|
||||
final int type = BLETypeConversions.toUnsigned(value, 2);
|
||||
final int encryption = BLETypeConversions.toUnsigned(value, 3);
|
||||
|
||||
if (header != 0) {
|
||||
LOG.warn("Non-zero header not supported");
|
||||
return true;
|
||||
}
|
||||
if (type != 2) {
|
||||
LOG.warn("Unsupported type {}", type);
|
||||
return true;
|
||||
}
|
||||
|
||||
final byte[] plainValue;
|
||||
if (encryption == 1) {
|
||||
plainValue = cipher.decrypt(ArrayUtils.subarray(value, 4, value.length));
|
||||
} else {
|
||||
plainValue = ArrayUtils.subarray(value, 4, value.length);
|
||||
}
|
||||
|
||||
LOG.debug("Got command: {}", GB.hexdump(plainValue));
|
||||
|
||||
final XiaomiProto.Command cmd;
|
||||
try {
|
||||
cmd = XiaomiProto.Command.parseFrom(plainValue);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse bytes as protobuf command payload", e);
|
||||
return true;
|
||||
}
|
||||
|
||||
final AbstractXiaomiService service = mServiceMap.get(cmd.getType());
|
||||
if (service != null) {
|
||||
service.handleCommand(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cmd.getType() == CMD_TYPE_AUTH) {
|
||||
cipher.handleAuthCommand(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG.warn("Unexpected watch command type {}", cmd.getType());
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG.warn("Unhandled characteristic changed: {} {}", characteristicUUID, GB.hexdump(value));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(final String config) {
|
||||
// TODO
|
||||
// TODO user info
|
||||
// TODO 24h
|
||||
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
final TransactionBuilder builder;
|
||||
try {
|
||||
builder = performInitialized("set time");
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to initialize transaction builder", e);
|
||||
return;
|
||||
}
|
||||
setCurrentTime(builder);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
public void setCurrentTime(final TransactionBuilder builder) {
|
||||
final Calendar now = GregorianCalendar.getInstance();
|
||||
final TimeZone tz = TimeZone.getDefault();
|
||||
|
||||
final GBPrefs gbPrefs = new GBPrefs(new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.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();
|
||||
|
||||
sendCommand(
|
||||
builder,
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(CMD_TYPE_SYSTEM)
|
||||
.setSubtype(CMD_SYSTEM_CLOCK)
|
||||
.setSystem(XiaomiProto.System.newBuilder().setClock(clock).build())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
final TransactionBuilder builder = createTransactionBuilder("test new function");
|
||||
sendCommand(
|
||||
builder,
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(CMD_TYPE_SYSTEM)
|
||||
.setSubtype(CMD_SYSTEM_DEVICE_INFO)
|
||||
.build()
|
||||
);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindPhone(final boolean start) {
|
||||
// TODO possible to notify watch?
|
||||
super.onFindPhone(start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(final boolean start) {
|
||||
// TODO onFindDevice
|
||||
super.onFindDevice(start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetPhoneVolume(final float volume) {
|
||||
musicService.onSetPhoneVolume(volume);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetGpsLocation(final Location location) {
|
||||
// TODO onSetGpsLocation
|
||||
super.onSetGpsLocation(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetReminders(final ArrayList<? extends Reminder> reminders) {
|
||||
scheduleService.onSetReminders(reminders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetWorldClocks(final ArrayList<? extends WorldClock> clocks) {
|
||||
scheduleService.onSetWorldClocks(clocks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(final NotificationSpec notificationSpec) {
|
||||
notificationService.onNotification(notificationSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(final int id) {
|
||||
notificationService.onDeleteNotification(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(final ArrayList<? extends Alarm> alarms) {
|
||||
scheduleService.onSetAlarms(alarms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(final CallSpec callSpec) {
|
||||
notificationService.onSetCallState(callSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
|
||||
notificationService.onSetCannedMessages(cannedMessagesSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(final MusicStateSpec stateSpec) {
|
||||
musicService.onSetMusicState(stateSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(final MusicSpec musicSpec) {
|
||||
musicService.onSetMusicInfo(musicSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(final Uri uri) {
|
||||
// TODO
|
||||
super.onInstallApp(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
// TODO
|
||||
super.onAppInfoReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppStart(final UUID uuid, boolean start) {
|
||||
// TODO
|
||||
super.onAppStart(uuid, start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDownload(final UUID uuid) {
|
||||
// TODO
|
||||
super.onAppDownload(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(final UUID uuid) {
|
||||
// TODO
|
||||
super.onAppDelete(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(final UUID appUuid, String config, Integer id) {
|
||||
// TODO
|
||||
super.onAppConfiguration(appUuid, config, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppReorder(final UUID[] uuids) {
|
||||
// TODO
|
||||
super.onAppReorder(uuids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(final int dataTypes) {
|
||||
// TODO
|
||||
super.onFetchRecordedData(dataTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(final int flags) {
|
||||
// TODO
|
||||
super.onReset(flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
healthService.onHeartRateTest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(final boolean enable) {
|
||||
healthService.onEnableRealtimeHeartRateMeasurement(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(final boolean enable) {
|
||||
healthService.onEnableRealtimeSteps(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenshotReq() {
|
||||
// TODO
|
||||
super.onScreenshotReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableHeartRateSleepSupport(final boolean enable) {
|
||||
// TODO
|
||||
super.onEnableHeartRateSleepSupport(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetHeartRateMeasurementInterval(final int seconds) {
|
||||
// TODO
|
||||
super.onSetHeartRateMeasurementInterval(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) {
|
||||
scheduleService.onAddCalendarEvent(calendarEventSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteCalendarEvent(final byte type, long id) {
|
||||
scheduleService.onDeleteCalendarEvent(type, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(final WeatherSpec weatherSpec) {
|
||||
weatherService.onSendWeather(weatherSpec);
|
||||
}
|
||||
|
||||
protected void phase2Initialize(final TransactionBuilder builder) {
|
||||
LOG.info("phase2Initialize");
|
||||
encryptedIndex = 1; // TODO not here
|
||||
|
||||
if (GBApplication.getPrefs().getBoolean("datetime_synconconnect", true)) {
|
||||
setCurrentTime(builder);
|
||||
}
|
||||
|
||||
for (final AbstractXiaomiService service : mServiceMap.values()) {
|
||||
service.initialize(builder);
|
||||
}
|
||||
|
||||
// request device info
|
||||
sendCommand(builder, CMD_TYPE_SYSTEM, CMD_SYSTEM_DEVICE_INFO);
|
||||
|
||||
// request battery status
|
||||
sendCommand(builder, CMD_TYPE_SYSTEM, CMD_SYSTEM_BATTERY);
|
||||
}
|
||||
|
||||
private void sendAck(final BluetoothGattCharacteristic characteristic) {
|
||||
final TransactionBuilder builder = createTransactionBuilder("send ack");
|
||||
builder.write(characteristic, PAYLOAD_ACK);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
protected void handleConfigCommand(final XiaomiProto.Command cmd) {
|
||||
switch (cmd.getSubtype()) {
|
||||
case CMD_SYSTEM_DEVICE_INFO:
|
||||
final XiaomiProto.DeviceInfo deviceInfo = cmd.getSystem().getDeviceInfo();
|
||||
final GBDeviceEventVersionInfo gbDeviceEventVersionInfo = new GBDeviceEventVersionInfo();
|
||||
gbDeviceEventVersionInfo.fwVersion = deviceInfo.getFirmware();
|
||||
//gbDeviceEventVersionInfo.fwVersion2 = "N/A";
|
||||
gbDeviceEventVersionInfo.hwVersion = deviceInfo.getModel();
|
||||
final GBDeviceEventUpdateDeviceInfo gbDeviceEventUpdateDeviceInfo = new GBDeviceEventUpdateDeviceInfo("SERIAL: ", deviceInfo.getSerialNumber());
|
||||
|
||||
evaluateGBDeviceEvent(gbDeviceEventVersionInfo);
|
||||
evaluateGBDeviceEvent(gbDeviceEventUpdateDeviceInfo);
|
||||
return;
|
||||
case CMD_SYSTEM_BATTERY:
|
||||
final XiaomiProto.Battery battery = cmd.getSystem().getPower().getBattery();
|
||||
final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo.batteryIndex = 0;
|
||||
batteryInfo.level = battery.getLevel();
|
||||
switch (battery.getState()) {
|
||||
case 1:
|
||||
batteryInfo.state = BatteryState.BATTERY_CHARGING;
|
||||
break;
|
||||
case 2:
|
||||
batteryInfo.state = BatteryState.BATTERY_NORMAL;
|
||||
break;
|
||||
default:
|
||||
batteryInfo.state = BatteryState.UNKNOWN;
|
||||
LOG.warn("Unknown battery state {}", battery.getState());
|
||||
}
|
||||
evaluateGBDeviceEvent(batteryInfo);
|
||||
return;
|
||||
case CMD_SYSTEM_CHARGER:
|
||||
// charger event, request battery state
|
||||
sendCommand(
|
||||
"request battery state",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(CMD_TYPE_SYSTEM)
|
||||
.setSubtype(CMD_SYSTEM_BATTERY)
|
||||
.build()
|
||||
);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unknown config command {}", cmd.getSubtype());
|
||||
}
|
||||
}
|
||||
|
||||
private short encryptedIndex = 0;
|
||||
|
||||
public void sendCommand(final String taskName, final XiaomiProto.Command command) {
|
||||
final TransactionBuilder builder = createTransactionBuilder(taskName);
|
||||
sendCommand(builder, command);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) {
|
||||
final byte[] commandBytes = command.toByteArray();
|
||||
final byte[] encryptedCommandBytes = cipher.encrypt(commandBytes, encryptedIndex);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(6 + encryptedCommandBytes.length).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.putShort((short) 0);
|
||||
buf.put((byte) 2); // 2 for command
|
||||
buf.put((byte) 1); // 1 for encrypted
|
||||
buf.putShort(encryptedIndex++);
|
||||
buf.put(encryptedCommandBytes);
|
||||
LOG.debug("Sending command {} as {}", GB.hexdump(commandBytes), GB.hexdump(buf.array()));
|
||||
builder.write(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), buf.array());
|
||||
}
|
||||
|
||||
public void sendCommand(final TransactionBuilder builder, final int type, final int subtype) {
|
||||
sendCommand(
|
||||
builder,
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(type)
|
||||
.setSubtype(subtype)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2023 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.xiaomi.services;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public abstract class AbstractXiaomiService {
|
||||
private final XiaomiSupport mSupport;
|
||||
|
||||
public AbstractXiaomiService(final XiaomiSupport support) {
|
||||
this.mSupport = support;
|
||||
}
|
||||
|
||||
public void setContext(final Context context) {
|
||||
|
||||
}
|
||||
|
||||
public abstract void handleCommand(final XiaomiProto.Command cmd);
|
||||
|
||||
public void initialize(final TransactionBuilder builder) {
|
||||
|
||||
}
|
||||
|
||||
protected XiaomiSupport getSupport() {
|
||||
return mSupport;
|
||||
}
|
||||
|
||||
protected XiaomiCoordinator getCoordinator() {
|
||||
return (XiaomiCoordinator) getSupport().getDevice().getDeviceCoordinator();
|
||||
}
|
||||
|
||||
protected Prefs getDevicePrefs() {
|
||||
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getSupport().getDevice().getAddress()));
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/* Copyright (C) 2023 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.xiaomi.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
|
||||
public class XiaomiHealthService extends AbstractXiaomiService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiHealthService.class);
|
||||
|
||||
public static final int COMMAND_TYPE = 8;
|
||||
|
||||
private static final int CMD_SET_USER_INFO = 0;
|
||||
|
||||
private static final int GENDER_MALE = 1;
|
||||
private static final int GENDER_FEMALE = 2;
|
||||
|
||||
public XiaomiHealthService(final XiaomiSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
// TODO
|
||||
LOG.warn("Unhandled health command");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(TransactionBuilder builder) {
|
||||
setUserInfo(builder);
|
||||
}
|
||||
|
||||
public void setUserInfo(final TransactionBuilder builder) {
|
||||
final ActivityUser activityUser = new ActivityUser();
|
||||
final int birthYear = activityUser.getYearOfBirth();
|
||||
final byte birthMonth = 7; // not in user attributes
|
||||
final byte birthDay = 1; // not in user attributes
|
||||
|
||||
final int genderInt = activityUser.getGender() != ActivityUser.GENDER_FEMALE ? GENDER_MALE : GENDER_FEMALE; // TODO other gender?
|
||||
|
||||
final Calendar now = GregorianCalendar.getInstance();
|
||||
final int age = now.get(Calendar.YEAR) - birthYear;
|
||||
// Compute the approximate max heart rate from the user age
|
||||
// TODO max heart rate should be input by the user
|
||||
int maxHeartRate = (int) Math.round(age <= 40 ? 220 - age : 207 - 0.7 * age);
|
||||
if (maxHeartRate < 100 || maxHeartRate > 220) {
|
||||
maxHeartRate = 175;
|
||||
}
|
||||
|
||||
final XiaomiProto.UserInfo userInfo = XiaomiProto.UserInfo.newBuilder()
|
||||
.setHeight(activityUser.getHeightCm())
|
||||
.setWeight(activityUser.getWeightKg())
|
||||
.setBirthday(Integer.parseInt(String.format(Locale.ROOT, "%02d%02d%02d", birthYear, birthMonth, birthDay)))
|
||||
.setGender(genderInt)
|
||||
.setMaxHeartRate(maxHeartRate)
|
||||
.setGoalCalories(activityUser.getCaloriesBurntGoal())
|
||||
.setGoalSteps(activityUser.getStepsGoal())
|
||||
.setGoalStanding(activityUser.getStandingTimeGoalHours())
|
||||
.setGoalMoving(activityUser.getActiveTimeGoalMinutes())
|
||||
.build();
|
||||
|
||||
final XiaomiProto.Health health = XiaomiProto.Health.newBuilder()
|
||||
.setUserInfo(userInfo)
|
||||
.build();
|
||||
|
||||
getSupport().sendCommand(
|
||||
builder,
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_SET_USER_INFO)
|
||||
.setHealth(health)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public void onHeartRateTest() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onEnableRealtimeHeartRateMeasurement(final boolean enable) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onEnableRealtimeSteps(final boolean enable) {
|
||||
// TODO
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/* Copyright (C) 2023 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.xiaomi.services;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
|
||||
|
||||
public class XiaomiMusicService extends AbstractXiaomiService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiMusicService.class);
|
||||
|
||||
public static final int COMMAND_TYPE = 18;
|
||||
|
||||
private static final int CMD_MUSIC_GET = 0;
|
||||
private static final int CMD_MUSIC_SEND = 1;
|
||||
private static final int CMD_MUSIC_BUTTON = 1;
|
||||
|
||||
private static final byte BUTTON_PLAY = 0x00;
|
||||
private static final byte BUTTON_PAUSE = 0x01;
|
||||
private static final byte BUTTON_PREVIOUS = 0x03;
|
||||
private static final byte BUTTON_NEXT = 0x04;
|
||||
private static final byte BUTTON_VOLUME = 0x05;
|
||||
|
||||
private static final byte STATE_NOTHING = 0x00;
|
||||
private static final byte STATE_PLAYING = 0x01;
|
||||
private static final byte STATE_PAUSED = 0x02;
|
||||
|
||||
protected MediaManager mediaManager = null;
|
||||
protected boolean isMusicAppStarted = false;
|
||||
|
||||
public XiaomiMusicService(final XiaomiSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(Context context) {
|
||||
super.setContext(context);
|
||||
this.mediaManager = new MediaManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
final XiaomiProto.Music music = cmd.getMusic();
|
||||
|
||||
switch (cmd.getSubtype()) {
|
||||
case CMD_MUSIC_GET:
|
||||
mediaManager.refresh();
|
||||
sendMusicStateToDevice();
|
||||
break;
|
||||
case CMD_MUSIC_BUTTON:
|
||||
final GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl();
|
||||
switch (music.getMediaKey().getKey()) {
|
||||
case BUTTON_PLAY:
|
||||
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAY;
|
||||
break;
|
||||
case BUTTON_PAUSE:
|
||||
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PAUSE;
|
||||
break;
|
||||
case BUTTON_PREVIOUS:
|
||||
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS;
|
||||
break;
|
||||
case BUTTON_NEXT:
|
||||
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT;
|
||||
break;
|
||||
case BUTTON_VOLUME:
|
||||
if (music.getMediaKey().getVolume() > 0) {
|
||||
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEUP;
|
||||
} else {
|
||||
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected media button key {}", music.getMediaKey().getKey());
|
||||
return;
|
||||
}
|
||||
// FIXME sometimes this is not triggering a device update?
|
||||
getSupport().evaluateGBDeviceEvent(deviceEventMusicControl);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unhandled music command {}", cmd.getSubtype());
|
||||
}
|
||||
}
|
||||
|
||||
public void onSetMusicState(final MusicStateSpec stateSpec) {
|
||||
if (mediaManager.onSetMusicState(stateSpec) && isMusicAppStarted) {
|
||||
sendMusicStateToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
public void onSetPhoneVolume(final float ignoredVolume) {
|
||||
sendMusicStateToDevice();
|
||||
}
|
||||
|
||||
public void onSetMusicInfo(final MusicSpec musicSpec) {
|
||||
if (mediaManager.onSetMusicInfo(musicSpec) && isMusicAppStarted) {
|
||||
sendMusicStateToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMusicStateToDevice() {
|
||||
final MusicSpec musicSpec = mediaManager.getBufferMusicSpec();
|
||||
final MusicStateSpec musicStateSpec = mediaManager.getBufferMusicStateSpec();
|
||||
|
||||
final XiaomiProto.MusicInfo.Builder musicInfo = XiaomiProto.MusicInfo.newBuilder()
|
||||
.setVolume(mediaManager.getPhoneVolume());
|
||||
|
||||
if (musicSpec == null || musicStateSpec == null) {
|
||||
musicInfo.setState(STATE_NOTHING);
|
||||
} else {
|
||||
if (musicStateSpec.state == MusicStateSpec.STATE_PLAYING) {
|
||||
musicInfo.setState(STATE_PLAYING);
|
||||
} else {
|
||||
musicInfo.setState(STATE_PAUSED);
|
||||
}
|
||||
|
||||
musicInfo.setVolume(mediaManager.getPhoneVolume())
|
||||
.setTrack(musicSpec.track)
|
||||
.setArtist(musicSpec.artist)
|
||||
.setPosition(musicStateSpec.position)
|
||||
.setDuration(musicSpec.duration);
|
||||
}
|
||||
|
||||
final XiaomiProto.Music music = XiaomiProto.Music.newBuilder()
|
||||
.setMusicInfo(musicInfo.build())
|
||||
.build();
|
||||
|
||||
getSupport().sendCommand(
|
||||
"send music",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_MUSIC_SEND)
|
||||
.setMusic(music)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* Copyright (C) 2023 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.xiaomi.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
|
||||
public class XiaomiNotificationService extends AbstractXiaomiService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiNotificationService.class);
|
||||
|
||||
public static final int COMMAND_TYPE = 7;
|
||||
|
||||
public XiaomiNotificationService(final XiaomiSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onNotification(final NotificationSpec notificationSpec) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onDeleteNotification(final int id) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onSetCallState(final CallSpec callSpec) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
|
||||
// TODO
|
||||
}
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
/* Copyright (C) 2023 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.xiaomi.services;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
|
||||
public class XiaomiScheduleService extends AbstractXiaomiService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiScheduleService.class);
|
||||
|
||||
public static final int COMMAND_TYPE = 17;
|
||||
|
||||
private static final int CMD_ALARMS_GET = 0;
|
||||
private static final int CMD_ALARMS_CREATE = 1;
|
||||
private static final int CMD_ALARMS_EDIT = 3;
|
||||
private static final int CMD_ALARMS_DELETE = 4;
|
||||
|
||||
private static final int REPETITION_ONCE = 0;
|
||||
private static final int REPETITION_DAILY = 1;
|
||||
private static final int REPETITION_WEEKLY = 5;
|
||||
private static final int REPETITION_MONTHLY = 7;
|
||||
private static final int REPETITION_YEARLY = 8;
|
||||
|
||||
private static final int ALARM_SMART = 1;
|
||||
private static final int ALARM_NORMAL = 2;
|
||||
|
||||
// Map of alarm position to Alarm, as returned by the band
|
||||
private final Map<Integer, Alarm> watchAlarms = new HashMap<>();
|
||||
|
||||
public XiaomiScheduleService(final XiaomiSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
switch (cmd.getSubtype()) {
|
||||
case CMD_ALARMS_GET:
|
||||
handleAlarms(cmd.getSchedule().getAlarms());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(final TransactionBuilder builder) {
|
||||
requestAlarms(builder);
|
||||
}
|
||||
|
||||
public void onSetReminders(final ArrayList<? extends Reminder> reminders) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onSetWorldClocks(final ArrayList<? extends WorldClock> clocks) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onSetAlarms(final ArrayList<? extends Alarm> alarms) {
|
||||
final List<Integer> alarmsToDelete = new ArrayList<>();
|
||||
|
||||
// TODO this is flaky, since it's the watch that defines the IDs...
|
||||
|
||||
for (final Alarm alarm : alarms) {
|
||||
final Alarm watchAlarm = watchAlarms.get(alarm.getPosition());
|
||||
|
||||
if (alarm.getUnused() && watchAlarm == null) {
|
||||
// Disabled on both
|
||||
continue;
|
||||
}
|
||||
|
||||
if (alarm.getUnused() && watchAlarm != null) {
|
||||
// Delete from watch
|
||||
alarmsToDelete.add(watchAlarm.getPosition() + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
final XiaomiProto.HourMinute hourMinute = XiaomiProto.HourMinute.newBuilder()
|
||||
.setHour(alarm.getHour())
|
||||
.setMinute(alarm.getMinute())
|
||||
.build();
|
||||
|
||||
final XiaomiProto.AlarmDetails.Builder alarmDetails = XiaomiProto.AlarmDetails.newBuilder()
|
||||
.setTime(hourMinute)
|
||||
.setEnabled(alarm.getEnabled())
|
||||
.setSmart(alarm.getSmartWakeup() ? ALARM_SMART : ALARM_NORMAL);
|
||||
|
||||
switch (alarm.getRepetition()) {
|
||||
case Alarm.ALARM_ONCE:
|
||||
alarmDetails.setRepeatMode(REPETITION_ONCE);
|
||||
break;
|
||||
case Alarm.ALARM_DAILY:
|
||||
alarmDetails.setRepeatMode(REPETITION_DAILY);
|
||||
break;
|
||||
default:
|
||||
alarmDetails.setRepeatMode(REPETITION_WEEKLY);
|
||||
alarmDetails.setRepeatFlags(alarm.getRepetition());
|
||||
break;
|
||||
}
|
||||
|
||||
final XiaomiProto.Schedule.Builder schedule = XiaomiProto.Schedule.newBuilder();
|
||||
|
||||
if (watchAlarm != null) {
|
||||
// update existing alarm
|
||||
schedule.setEditAlarm(
|
||||
XiaomiProto.Alarm.newBuilder()
|
||||
.setId(alarm.getPosition() + 1)
|
||||
.setAlarmDetails(alarmDetails)
|
||||
.build()
|
||||
);
|
||||
} else {
|
||||
schedule.setCreateAlarm(alarmDetails);
|
||||
}
|
||||
|
||||
getSupport().sendCommand(
|
||||
(watchAlarm == null ? "create" : "update") + " alarm " + alarm.getPosition(),
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(watchAlarm == null ? CMD_ALARMS_CREATE : CMD_ALARMS_EDIT)
|
||||
.setSchedule(schedule)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
if (!alarmsToDelete.isEmpty()) {
|
||||
final XiaomiProto.AlarmDelete alarmDelete = XiaomiProto.AlarmDelete.newBuilder()
|
||||
.addAllId(alarmsToDelete)
|
||||
.build();
|
||||
|
||||
final XiaomiProto.Schedule schedule = XiaomiProto.Schedule.newBuilder()
|
||||
.setDeleteAlarm(alarmDelete)
|
||||
.build();
|
||||
|
||||
getSupport().sendCommand(
|
||||
"delete unused alarms",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_ALARMS_DELETE)
|
||||
.setSchedule(schedule)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void requestAlarms(final TransactionBuilder builder) {
|
||||
getSupport().sendCommand(builder, COMMAND_TYPE, CMD_ALARMS_GET);
|
||||
}
|
||||
|
||||
public void handleAlarms(final XiaomiProto.Alarms alarms) {
|
||||
LOG.debug("Got {} alarms", alarms.getAlarmCount());
|
||||
|
||||
watchAlarms.clear();
|
||||
for (final XiaomiProto.Alarm alarm : alarms.getAlarmList()) {
|
||||
final nodomain.freeyourgadget.gadgetbridge.entities.Alarm gbAlarm = new nodomain.freeyourgadget.gadgetbridge.entities.Alarm();
|
||||
gbAlarm.setUnused(false); // If the band sent it, it's not unused
|
||||
gbAlarm.setPosition(alarm.getId() - 1); // band id starts at 1
|
||||
gbAlarm.setEnabled(alarm.getAlarmDetails().getEnabled());
|
||||
gbAlarm.setSmartWakeup(alarm.getAlarmDetails().getSmart() == ALARM_SMART);
|
||||
gbAlarm.setHour(alarm.getAlarmDetails().getTime().getHour());
|
||||
gbAlarm.setMinute(alarm.getAlarmDetails().getTime().getMinute());
|
||||
switch (alarm.getAlarmDetails().getRepeatMode()) {
|
||||
case REPETITION_ONCE:
|
||||
gbAlarm.setRepetition(Alarm.ALARM_ONCE);
|
||||
break;
|
||||
case REPETITION_DAILY:
|
||||
gbAlarm.setRepetition(Alarm.ALARM_DAILY);
|
||||
break;
|
||||
case REPETITION_WEEKLY:
|
||||
gbAlarm.setRepetition(alarm.getAlarmDetails().getRepeatFlags());
|
||||
break;
|
||||
}
|
||||
|
||||
watchAlarms.put(gbAlarm.getPosition(), gbAlarm);
|
||||
}
|
||||
|
||||
final List<nodomain.freeyourgadget.gadgetbridge.entities.Alarm> dbAlarms = DBHelper.getAlarms(getSupport().getDevice());
|
||||
int numUpdatedAlarms = 0;
|
||||
|
||||
for (nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm : dbAlarms) {
|
||||
final int pos = alarm.getPosition();
|
||||
final Alarm updatedAlarm = watchAlarms.get(pos);
|
||||
final boolean alarmNeedsUpdate = updatedAlarm == null ||
|
||||
alarm.getUnused() != updatedAlarm.getUnused() ||
|
||||
alarm.getEnabled() != updatedAlarm.getEnabled() ||
|
||||
alarm.getSmartWakeup() != updatedAlarm.getSmartWakeup() ||
|
||||
alarm.getHour() != updatedAlarm.getHour() ||
|
||||
alarm.getMinute() != updatedAlarm.getMinute() ||
|
||||
alarm.getRepetition() != updatedAlarm.getRepetition();
|
||||
|
||||
if (alarmNeedsUpdate) {
|
||||
numUpdatedAlarms++;
|
||||
LOG.info("Updating alarm index={}, unused={}", pos, updatedAlarm == null);
|
||||
alarm.setUnused(updatedAlarm == null);
|
||||
if (updatedAlarm != null) {
|
||||
alarm.setEnabled(updatedAlarm.getEnabled());
|
||||
alarm.setSmartWakeup(updatedAlarm.getSmartWakeup());
|
||||
alarm.setHour(updatedAlarm.getHour());
|
||||
alarm.setMinute(updatedAlarm.getMinute());
|
||||
alarm.setRepetition(updatedAlarm.getRepetition());
|
||||
}
|
||||
DBHelper.store(alarm);
|
||||
}
|
||||
}
|
||||
|
||||
if (numUpdatedAlarms > 0) {
|
||||
final Intent intent = new Intent(DeviceService.ACTION_SAVE_ALARMS);
|
||||
LocalBroadcastManager.getInstance(getSupport().getContext()).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onDeleteCalendarEvent(final byte type, long id) {
|
||||
// TODO
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/* Copyright (C) 2023 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.xiaomi.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
|
||||
public class XiaomiSystemService extends AbstractXiaomiService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSystemService.class);
|
||||
|
||||
public static final int COMMAND_TYPE = 2;
|
||||
|
||||
public XiaomiSystemService(final XiaomiSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
// TODO
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/* Copyright (C) 2023 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.xiaomi.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
|
||||
public class XiaomiWeatherService extends AbstractXiaomiService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiWeatherService.class);
|
||||
|
||||
public static final int COMMAND_TYPE = 10;
|
||||
|
||||
public XiaomiWeatherService(final XiaomiSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void onSendWeather(final WeatherSpec weatherSpec) {
|
||||
// TODO
|
||||
}
|
||||
}
|
625
app/src/main/proto/xiaomi.proto
Normal file
625
app/src/main/proto/xiaomi.proto
Normal file
@ -0,0 +1,625 @@
|
||||
syntax = "proto2"; // we must use proto2 to serialize default values on the wire
|
||||
|
||||
package xiaomi;
|
||||
|
||||
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.xiaomi";
|
||||
option java_outer_classname = "XiaomiProto";
|
||||
|
||||
message Command {
|
||||
required uint32 type = 1;
|
||||
optional uint32 subtype = 2;
|
||||
|
||||
optional Auth auth = 3;
|
||||
optional System system = 4;
|
||||
optional Health health = 10;
|
||||
optional Music music = 20;
|
||||
optional Notification notification = 9;
|
||||
optional Weather weather = 12;
|
||||
optional Schedule schedule = 19;
|
||||
|
||||
optional uint32 status = 100; // 0 on success on some
|
||||
}
|
||||
|
||||
//
|
||||
// Auth
|
||||
//
|
||||
|
||||
message Auth {
|
||||
// 1, 26
|
||||
optional PhoneNonce phoneNonce = 30;
|
||||
optional WatchNonce watchNonce = 31;
|
||||
// 1, 27
|
||||
optional AuthStep3 authStep3 = 32;
|
||||
optional AuthStep4 authStep4 = 33;
|
||||
}
|
||||
|
||||
message PhoneNonce {
|
||||
required bytes nonce = 1;
|
||||
}
|
||||
|
||||
message WatchNonce {
|
||||
required bytes nonce = 1;
|
||||
required bytes hmac = 2;
|
||||
}
|
||||
|
||||
message AuthStep3 {
|
||||
required bytes encryptedNonces = 1;
|
||||
required bytes encryptedDeviceInfo = 2; // AuthDeviceInfo
|
||||
}
|
||||
|
||||
message AuthStep4 {
|
||||
required uint32 unknown1 = 1;
|
||||
required uint32 unknown2 = 2;
|
||||
}
|
||||
|
||||
message AuthDeviceInfo {
|
||||
required uint32 unknown1 = 1; // 0 - needs to be serialized explicitly
|
||||
required float phoneApiLevel = 2;
|
||||
required string phoneName = 3; // phone model
|
||||
required uint32 unknown3 = 4; // 224
|
||||
required string region = 5; // 2-letter, upper case
|
||||
}
|
||||
|
||||
//
|
||||
// System
|
||||
//
|
||||
|
||||
message System {
|
||||
// 2, 1
|
||||
optional Power power = 2;
|
||||
// 2, 2
|
||||
optional DeviceInfo deviceInfo = 3;
|
||||
// 2, 3
|
||||
optional Clock clock = 4;
|
||||
|
||||
// 2, 18
|
||||
optional uint32 findDevice = 5; // 0
|
||||
|
||||
// 2, 29
|
||||
optional DisplayItems displayItems = 10;
|
||||
|
||||
// 2, 34
|
||||
optional DoNotDisturb dndStatus = 11;
|
||||
|
||||
// 2, 9 get | 2, 21 set
|
||||
optional Password password = 19;
|
||||
|
||||
// 2, 7 get | 2, 8 set
|
||||
optional Camera camera = 15;
|
||||
|
||||
// 2, 51
|
||||
optional Widgets widgets = 28;
|
||||
// 2, 53
|
||||
optional WidgetsSingle widgetsSingle = 29;
|
||||
|
||||
// 2, 14
|
||||
optional DoNotDisturb dnd2 = 34;
|
||||
// 2, 15
|
||||
optional DndSync dndSync = 35;
|
||||
|
||||
// 2, 46
|
||||
optional VibrationPatterns vibrationPatterns = 38;
|
||||
|
||||
// 2, 47
|
||||
optional VibrationNotificationType vibrationSetPreset = 39;
|
||||
|
||||
// 2, 58
|
||||
optional CustomVibrationPattern vibrationPatternCreate = 40;
|
||||
|
||||
// 2, 59
|
||||
optional VibrationTest vibrationTestCustom = 41;
|
||||
|
||||
// 2, 47
|
||||
optional VibrationPatternAck vibrationPatternAck = 43;
|
||||
|
||||
// 2, 79
|
||||
optional Charger charger = 49;
|
||||
}
|
||||
|
||||
message Power {
|
||||
optional Battery battery = 1;
|
||||
}
|
||||
|
||||
message Battery {
|
||||
optional uint32 level = 1;
|
||||
optional uint32 state = 2;
|
||||
optional LastCharge lastCharge = 3;
|
||||
}
|
||||
|
||||
message LastCharge {
|
||||
optional uint32 state = 1; // 2
|
||||
optional uint32 timestampSeconds = 2;
|
||||
}
|
||||
|
||||
message DeviceInfo {
|
||||
required string serialNumber = 1;
|
||||
required string firmware = 2;
|
||||
optional string unknown3 = 3; // "" ?
|
||||
required string model = 4;
|
||||
}
|
||||
|
||||
message Clock {
|
||||
required Date date = 1;
|
||||
required Time time = 2;
|
||||
required TimeZone timezone = 3;
|
||||
required bool isNot24hour = 4;
|
||||
}
|
||||
|
||||
message Date {
|
||||
required uint32 year = 1;
|
||||
required uint32 month = 2;
|
||||
required uint32 day = 3;
|
||||
}
|
||||
|
||||
message Time {
|
||||
required uint32 hour = 1;
|
||||
required uint32 minute = 2;
|
||||
optional uint32 second = 3;
|
||||
optional uint32 millisecond = 4;
|
||||
}
|
||||
|
||||
message TimeZone {
|
||||
// offsets are in blocks of 15 min
|
||||
optional sint32 zoneOffset = 1;
|
||||
optional sint32 dstOffset = 2;
|
||||
required string name = 3;
|
||||
}
|
||||
|
||||
message DisplayItems {
|
||||
repeated DisplayItem displayItem = 1;
|
||||
}
|
||||
|
||||
message DisplayItem {
|
||||
optional string code = 1;
|
||||
optional string name = 2;
|
||||
optional bool disabled = 3;
|
||||
optional uint32 isSettings = 4;
|
||||
optional uint32 unknown5 = 5; // 1
|
||||
optional bool rarelyUsed = 6;
|
||||
}
|
||||
|
||||
message Camera {
|
||||
required bool enabled = 1;
|
||||
}
|
||||
|
||||
message Widgets {
|
||||
repeated Widget widget = 1;
|
||||
}
|
||||
|
||||
message Widget {
|
||||
optional uint32 unknown1 = 1;
|
||||
optional uint32 unknown2 = 2;
|
||||
repeated WidgetPart widgetPart = 3;
|
||||
}
|
||||
|
||||
message WidgetPart {
|
||||
optional uint32 unknown1 = 1;
|
||||
optional uint32 unknown2 = 2;
|
||||
optional uint32 unknown3 = 3;
|
||||
}
|
||||
|
||||
message WidgetsSingle {
|
||||
repeated SingleWidget widget = 1;
|
||||
}
|
||||
|
||||
message SingleWidget {
|
||||
optional uint32 unknown1 = 1;
|
||||
optional uint32 unknown2 = 2;
|
||||
optional uint32 unknown3 = 3;
|
||||
optional string title = 4;
|
||||
optional uint32 unknown5 = 5;
|
||||
}
|
||||
|
||||
message DoNotDisturb {
|
||||
optional uint32 status = 1; // 0 enabled, 2 disabled
|
||||
}
|
||||
|
||||
message DoNotDisturb2 {
|
||||
}
|
||||
|
||||
message DndSync {
|
||||
}
|
||||
|
||||
message Password {
|
||||
optional uint32 state = 1; // 1 disabled, 2 enabled
|
||||
optional string password = 2;
|
||||
}
|
||||
|
||||
message VibrationPatterns {
|
||||
repeated VibrationNotificationType notificationType = 1;
|
||||
optional uint32 unknown2 = 2; // 50, max patterns?
|
||||
repeated CustomVibrationPattern customVibrationPattern = 3;
|
||||
}
|
||||
|
||||
message CustomVibrationPattern {
|
||||
optional uint32 id = 1;
|
||||
optional string name = 2;
|
||||
repeated Vibration vibration = 3;
|
||||
optional uint32 unknown4 = 4; // 1 on creation
|
||||
}
|
||||
|
||||
message VibrationNotificationType {
|
||||
// 1 incoming calls
|
||||
// 2 events
|
||||
// 3 alarms
|
||||
// 4 notifications
|
||||
// 5 standing reminder
|
||||
// 6 sms
|
||||
// 7 goal
|
||||
// 8 events
|
||||
optional uint32 notificationType = 1;
|
||||
optional uint32 preset = 2;
|
||||
}
|
||||
|
||||
message VibrationTest {
|
||||
repeated Vibration vibration = 1;
|
||||
}
|
||||
|
||||
message VibrationPatternAck {
|
||||
optional uint32 status = 1; // 0
|
||||
}
|
||||
|
||||
message Vibration {
|
||||
optional uint32 vibrate = 1; // 0/1
|
||||
optional uint32 ms = 2;
|
||||
}
|
||||
|
||||
message Charger {
|
||||
optional uint32 state = 1; // 1 charging, 2 not charging
|
||||
}
|
||||
|
||||
//
|
||||
// Health
|
||||
//
|
||||
|
||||
message Health {
|
||||
optional UserInfo userInfo = 1;
|
||||
optional SpO2 spo2 = 7;
|
||||
optional HeartRate heartRate = 8;
|
||||
// 8, 12 get | 8, 13 set
|
||||
optional StandingReminder standingReminder = 9;
|
||||
optional Stress stress = 10;
|
||||
optional AchievementReminders achievementReminders = 13;
|
||||
|
||||
// 8, 35 get | 8, 36 set
|
||||
optional VitalityScore vitalityScore = 14;
|
||||
|
||||
// 8, 26
|
||||
optional WorkoutStatusWatch workoutStatusWatch = 20;
|
||||
|
||||
// 8, 30
|
||||
optional WorkoutOpenWatch workoutOpenWatch = 25;
|
||||
optional WorkoutOpenReply workoutOpenReply = 26;
|
||||
|
||||
// 7, 48
|
||||
optional WorkoutLocation workoutLocation = 40;
|
||||
|
||||
// 8,45 enable | 8, 46 disable | 8, 47 periodic
|
||||
optional RealTimeStats realTimeStats = 39;
|
||||
}
|
||||
|
||||
message UserInfo {
|
||||
optional uint32 height = 1; // cm
|
||||
optional float weight = 2; // kg
|
||||
optional uint32 birthday = 3; // YYYYMMDD
|
||||
optional uint32 gender = 4; // 1 male, 2 female
|
||||
optional uint32 maxHeartRate = 5;
|
||||
optional uint32 goalCalories = 6;
|
||||
optional uint32 goalSteps = 7;
|
||||
optional uint32 goalStanding = 9; // hours
|
||||
optional uint32 goalMoving = 11; // minutes
|
||||
}
|
||||
|
||||
message SpO2 {
|
||||
optional uint32 unknown1 = 1; // 1
|
||||
optional bool allDayTracking = 2;
|
||||
optional Spo2AlarmLow alarmLow = 4;
|
||||
}
|
||||
|
||||
message Spo2AlarmLow {
|
||||
optional bool alarmLowEnabled = 1;
|
||||
optional uint32 alarmLowThreshold = 2; // 90, 85, 80
|
||||
}
|
||||
|
||||
message HeartRate {
|
||||
optional bool disabled = 1; // 0 enabled 1 disabled
|
||||
optional uint32 interval = 2; // 0 smart 1 10 30
|
||||
optional bool alarmHighEnabled = 3;
|
||||
optional uint32 alarmHighThreshold = 4; // 100, 110, ... 150
|
||||
optional AdvancedMonitoring advancedMonitoring = 5;
|
||||
optional uint32 unknown7 = 7; // 1
|
||||
optional HeartRateAlarmLow heartRateAlarmLow = 8;
|
||||
required uint32 breathingScore = 9; // 1 on, 2 off
|
||||
}
|
||||
|
||||
message AdvancedMonitoring {
|
||||
required bool enabled = 1;
|
||||
}
|
||||
|
||||
message HeartRateAlarmLow {
|
||||
optional bool alarmLowEnabled = 1;
|
||||
optional uint32 alarmLowThreshold = 2; // 40, 45, 50
|
||||
}
|
||||
|
||||
message StandingReminder {
|
||||
optional bool enabled = 1;
|
||||
optional HourMinute start = 2;
|
||||
optional HourMinute end = 3;
|
||||
optional bool dnd = 4;
|
||||
optional HourMinute dndStart = 6;
|
||||
optional HourMinute dndEnd = 7;
|
||||
}
|
||||
|
||||
message Stress {
|
||||
optional bool allDayTracking = 1;
|
||||
optional RelaxReminder relaxReminder = 2;
|
||||
}
|
||||
|
||||
message AchievementReminders {
|
||||
optional bool enabled = 1;
|
||||
optional uint32 suggested = 2; // 0 moving, 1 standing
|
||||
}
|
||||
|
||||
message RelaxReminder {
|
||||
optional bool enabled = 1;
|
||||
optional uint32 unknown2 = 2; // 0
|
||||
}
|
||||
|
||||
message VitalityScore {
|
||||
optional bool sevenDay = 1;
|
||||
optional bool dailyProgress = 2;
|
||||
}
|
||||
|
||||
message WorkoutStatusWatch {
|
||||
optional uint32 timestamp = 1; // seconds
|
||||
optional uint32 unknown2 = 2;
|
||||
}
|
||||
|
||||
message WorkoutOpenWatch {
|
||||
optional uint32 unknown1 = 1; // 2
|
||||
optional uint32 unknown2 = 2; // 2
|
||||
}
|
||||
|
||||
message WorkoutOpenReply {
|
||||
// 3 2 10
|
||||
// ...
|
||||
// 0 2 10
|
||||
// 0 2 2
|
||||
optional uint32 unknown1 = 1;
|
||||
optional uint32 unknown2 = 2;
|
||||
optional uint32 unknown3 = 3;
|
||||
}
|
||||
|
||||
message WorkoutLocation {
|
||||
optional uint32 unknown1 = 1; // 10, sometimes 2
|
||||
optional uint32 timestamp = 2; // seconds
|
||||
optional double longitude = 3;
|
||||
optional double latitude = 4;
|
||||
optional float unknown6 = 6; // ?
|
||||
optional float unknown7 = 7; // altitude?
|
||||
optional float unknown8 = 8; // ?
|
||||
optional float unknown9 = 9; // ?
|
||||
}
|
||||
|
||||
message RealTimeStats {
|
||||
optional uint32 steps = 1;
|
||||
optional uint32 calories = 2;
|
||||
optional uint32 unknown3 = 3; // increases during activity
|
||||
optional uint32 heartRate = 4;
|
||||
optional uint32 unknown5 = 5; // 0 probably moving time
|
||||
optional uint32 standingHours = 6;
|
||||
}
|
||||
|
||||
//
|
||||
// Music
|
||||
//
|
||||
|
||||
message Music {
|
||||
// 18, 1
|
||||
optional MusicInfo musicInfo = 1;
|
||||
// 18, 2
|
||||
optional MediaKey mediaKey = 2;
|
||||
}
|
||||
|
||||
message MusicInfo {
|
||||
required uint32 state = 1; // 0 not playing, 1 playing, 2 paused
|
||||
optional uint32 volume = 2;
|
||||
optional string track = 4;
|
||||
optional string artist = 5;
|
||||
optional uint32 position = 6;
|
||||
optional uint32 duration = 7;
|
||||
}
|
||||
|
||||
message MediaKey {
|
||||
required uint32 key = 1; // 0 play, 1 pause, 3 prev, 4 next, 5 vol
|
||||
optional uint32 volume = 2; // 100 vol+, 0 vol-
|
||||
}
|
||||
|
||||
//
|
||||
// Notification
|
||||
//
|
||||
|
||||
message Notification {
|
||||
optional Notification2 notification2 = 3;
|
||||
optional Notification4 notification4 = 4;
|
||||
// 7, 12
|
||||
optional CannedReplies cannedReplies = 9;
|
||||
}
|
||||
|
||||
message Notification2 {
|
||||
optional Notification3 notification3 = 1;
|
||||
}
|
||||
|
||||
message Notification3 {
|
||||
optional string package = 1;
|
||||
optional string appName = 2;
|
||||
optional string title = 3;
|
||||
optional string timestamp = 6;
|
||||
optional string unknown4 = 4;
|
||||
optional string body = 5;
|
||||
optional uint32 id = 7;
|
||||
optional string unknown12 = 12;
|
||||
optional uint32 hasReply = 13;
|
||||
}
|
||||
|
||||
message Notification4 {
|
||||
optional Notification5 notification5 = 1;
|
||||
}
|
||||
|
||||
message CannedReplies {
|
||||
optional uint32 minReplies = 1;
|
||||
repeated string reply = 2;
|
||||
optional uint32 maxReplies = 3;
|
||||
}
|
||||
|
||||
message Notification5 {
|
||||
optional uint32 id = 1;
|
||||
optional string package = 2;
|
||||
optional string unknown4 = 4;
|
||||
}
|
||||
|
||||
//
|
||||
// Weather
|
||||
//
|
||||
|
||||
message Weather {
|
||||
optional WeatherCurrent current = 1;
|
||||
optional WeatherDaily daily = 2;
|
||||
|
||||
// 10, 6 request without payload?
|
||||
|
||||
// 10, 5 set current | 10, 7 create | 10, 8 delete
|
||||
optional WeatherCurrentLocation currentLocation = 4;
|
||||
// 10, 7 create
|
||||
optional WeatherLocation create = 5;
|
||||
|
||||
// 10, 10
|
||||
optional WeatherTemperatureUnit temperatureUnit = 6;
|
||||
}
|
||||
|
||||
message WeatherCurrent {
|
||||
}
|
||||
|
||||
message WeatherDaily {
|
||||
}
|
||||
|
||||
message WeatherCurrentLocation {
|
||||
optional WeatherLocation location = 1;
|
||||
}
|
||||
|
||||
message WeatherLocation {
|
||||
optional string code = 1;
|
||||
optional string name = 2;
|
||||
}
|
||||
|
||||
message WeatherUnknown1 {
|
||||
optional float unknown12 = 12;
|
||||
}
|
||||
|
||||
message WeatherTemperatureUnit {
|
||||
optional uint32 unit = 1; // 1 celsius 2 fahrenheit
|
||||
}
|
||||
|
||||
//
|
||||
// Schedule
|
||||
//
|
||||
|
||||
message Schedule {
|
||||
// 17, 0 get
|
||||
optional Alarms alarms = 1;
|
||||
// 17, 1
|
||||
optional AlarmDetails createAlarm = 2;
|
||||
// 17, 3 -> returns 17, 5
|
||||
optional Alarm editAlarm = 3;
|
||||
|
||||
optional uint32 ackId = 4; // id of created or edited alarm and event
|
||||
|
||||
// 17, 4
|
||||
optional AlarmDelete deleteAlarm = 5;
|
||||
|
||||
// 17, 8 get | 17, 9 set
|
||||
optional SleepMode sleepMode = 9;
|
||||
|
||||
// 17, 14 get: 10 -> 2: 50 // max events?
|
||||
optional Events events = 10;
|
||||
|
||||
// 17,10 get/ret | 17,11 create | 17,13 delete
|
||||
optional WorldClocks worldClocks = 11;
|
||||
|
||||
optional uint32 worldClockStatus = 13; // 0 on edit and create
|
||||
|
||||
// 17, 15
|
||||
optional EventDetails createEvent = 14;
|
||||
|
||||
// 17, 17
|
||||
optional Event editEvent = 15;
|
||||
|
||||
// 17, 18
|
||||
optional EventDelete deleteEvent = 17;
|
||||
}
|
||||
|
||||
message Alarms {
|
||||
optional uint32 maxAlarms = 2; // 10
|
||||
optional uint32 unknown3 = 3; // 0
|
||||
optional uint32 unknown4 = 4; // 1
|
||||
repeated Alarm alarm = 1;
|
||||
}
|
||||
|
||||
message Alarm {
|
||||
optional uint32 id = 1; // starts at 1
|
||||
optional AlarmDetails alarmDetails = 2;
|
||||
}
|
||||
|
||||
message AlarmDetails {
|
||||
optional HourMinute time = 2;
|
||||
optional uint32 repeatMode = 3; // 0 once, 1 daily, 5 weekly
|
||||
optional uint32 repeatFlags = 4; // only if weekly: 31 during week, 1 monday, 2 tuesday, 3 mon tue
|
||||
optional bool enabled = 5;
|
||||
optional uint32 smart = 7; // 1 smart, 2 normal
|
||||
}
|
||||
|
||||
message AlarmDelete {
|
||||
repeated uint32 id = 1;
|
||||
}
|
||||
|
||||
message SleepMode {
|
||||
required bool enabled = 1;
|
||||
optional SleepModeSchedule schedule = 2;
|
||||
}
|
||||
|
||||
message SleepModeSchedule {
|
||||
optional HourMinute start = 1;
|
||||
optional HourMinute end = 2;
|
||||
optional uint32 unknown3 = 3; // 0
|
||||
}
|
||||
|
||||
message Events {
|
||||
repeated Event event = 1;
|
||||
optional uint32 unknown2 = 2; // 50, max events?
|
||||
}
|
||||
|
||||
message Event {
|
||||
optional uint32 id = 1;
|
||||
optional EventDetails eventDetails = 2;
|
||||
}
|
||||
|
||||
message EventDetails {
|
||||
optional Date date = 1;
|
||||
optional Time time = 2;
|
||||
optional uint32 repeatMode = 3; // 0 once, 1 daily, weekly (every monday), 7 monthly, 8 yearly
|
||||
optional uint32 repeatFlags = 4; // 64 for unset, day flags on weekly
|
||||
optional string title = 5;
|
||||
}
|
||||
|
||||
message EventDelete {
|
||||
repeated uint32 id = 1;
|
||||
}
|
||||
|
||||
message WorldClocks {
|
||||
repeated string worldClock = 1;
|
||||
}
|
||||
|
||||
message HourMinute {
|
||||
required uint32 hour = 1;
|
||||
required uint32 minute = 2;
|
||||
}
|
@ -1296,6 +1296,7 @@
|
||||
<string name="devicetype_miband5">Mi Band 5</string>
|
||||
<string name="devicetype_miband6">Mi Band 6</string>
|
||||
<string name="devicetype_miband7">Xiaomi Smart Band 7</string>
|
||||
<string name="devicetype_miband8">Xiaomi Smart Band 8</string>
|
||||
<string name="devicetype_amazfit_balance">Amazfit Balance</string>
|
||||
<string name="devicetype_amazfit_active">Amazfit Active</string>
|
||||
<string name="devicetype_amazfit_active_edge">Amazfit Active Edge</string>
|
||||
@ -2390,6 +2391,7 @@
|
||||
<string name="temperature_scale_cf_summary">Select whether device uses Celsius or Fahrenheit scale.</string>
|
||||
<string name="temperature_scale_celsius">Celsius</string>
|
||||
<string name="temperature_scale_fahrenheit">Fahrenheit</string>
|
||||
|
||||
<string name="fossil_hr_nav_app_not_installed_notify_title">Navigation app not installed on watch</string>
|
||||
<string name="fossil_hr_nav_app_not_installed_notify_text">Navigation started but navigationApp not installed on watch. Please install it from the App Manager.</string>
|
||||
<string name="call_rejection_method_reject">Reject</string>
|
||||
@ -2404,4 +2406,5 @@
|
||||
<string name="pref_title_navigation_apps">Navigation apps</string>
|
||||
<string name="pref_navigation_app_osmand">OsmAnd(+)</string>
|
||||
<string name="pref_navigation_app_gmaps">Google Maps</string>
|
||||
<string name="serial_number">Serial Number</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user