mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-27 04:16:49 +01:00
Huawei: Add initial support for Huawei-Honor
This commit is contained in:
parent
ab894ae433
commit
0c22ecdd51
@ -45,7 +45,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
final Schema schema = new Schema(66, MAIN_PACKAGE + ".entities");
|
||||
final Schema schema = new Schema(67, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
@ -109,6 +109,12 @@ public class GBDaoGenerator {
|
||||
addWena3StressSample(schema, user, device);
|
||||
addFemometerVinca2TemperatureSample(schema, user, device);
|
||||
|
||||
addHuaweiActivitySample(schema, user, device);
|
||||
|
||||
Entity huaweiWorkoutSummary = addHuaweiWorkoutSummarySample(schema, user, device);
|
||||
addHuaweiWorkoutDataSample(schema, user, device, huaweiWorkoutSummary);
|
||||
addHuaweiWorkoutPaceSample(schema, user, device, huaweiWorkoutSummary);
|
||||
|
||||
addCalendarSyncState(schema, device);
|
||||
addAlarms(schema, user, device);
|
||||
addReminders(schema, user, device);
|
||||
@ -930,6 +936,7 @@ public class GBDaoGenerator {
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
|
||||
private static Entity addWithingsSteelHRActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "WithingsSteelHRActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
@ -1011,6 +1018,99 @@ public class GBDaoGenerator {
|
||||
return perAppSetting;
|
||||
}
|
||||
|
||||
private static Entity addHuaweiActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "HuaweiActivitySample");
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty("otherTimestamp").notNull().primaryKey();
|
||||
activitySample.addByteProperty("source").notNull().primaryKey();
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("calories").notNull();
|
||||
activitySample.addIntProperty("distance").notNull();
|
||||
activitySample.addIntProperty("spo").notNull();
|
||||
activitySample.addIntProperty("heartRate").notNull();
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addHuaweiWorkoutSummarySample(Schema schema, Entity user, Entity device) {
|
||||
Entity workoutSummary = addEntity(schema, "HuaweiWorkoutSummarySample");
|
||||
|
||||
workoutSummary.setJavaDoc("Contains Huawei Workout Summary samples (one per workout)");
|
||||
|
||||
workoutSummary.addLongProperty("workoutId").primaryKey().autoincrement();
|
||||
|
||||
Property deviceId = workoutSummary.addLongProperty("deviceId").notNull().getProperty();
|
||||
workoutSummary.addToOne(device, deviceId);
|
||||
Property userId = workoutSummary.addLongProperty("userId").notNull().getProperty();
|
||||
workoutSummary.addToOne(user, userId);
|
||||
|
||||
workoutSummary.addShortProperty("workoutNumber").notNull();
|
||||
workoutSummary.addByteProperty("status").notNull();
|
||||
workoutSummary.addIntProperty("startTimestamp").notNull();
|
||||
workoutSummary.addIntProperty("endTimestamp").notNull();
|
||||
workoutSummary.addIntProperty("calories").notNull();
|
||||
workoutSummary.addIntProperty("distance").notNull();
|
||||
workoutSummary.addIntProperty("stepCount").notNull();
|
||||
workoutSummary.addIntProperty("totalTime").notNull();
|
||||
workoutSummary.addIntProperty("duration").notNull();
|
||||
workoutSummary.addByteProperty("type").notNull();
|
||||
workoutSummary.addShortProperty("strokes").notNull();
|
||||
workoutSummary.addShortProperty("avgStrokeRate").notNull();
|
||||
workoutSummary.addShortProperty("poolLength").notNull();
|
||||
workoutSummary.addShortProperty("laps").notNull();
|
||||
workoutSummary.addShortProperty("avgSwolf").notNull();
|
||||
|
||||
workoutSummary.addByteArrayProperty("rawData");
|
||||
|
||||
return workoutSummary;
|
||||
}
|
||||
|
||||
private static Entity addHuaweiWorkoutDataSample(Schema schema, Entity user, Entity device, Entity summaryEntity) {
|
||||
Entity workoutDataSample = addEntity(schema, "HuaweiWorkoutDataSample");
|
||||
|
||||
workoutDataSample.setJavaDoc("Contains Huawei Workout data samples (multiple per workout)");
|
||||
|
||||
Property id = workoutDataSample.addLongProperty("workoutId").primaryKey().notNull().getProperty();
|
||||
workoutDataSample.addToOne(summaryEntity, id);
|
||||
|
||||
workoutDataSample.addIntProperty("timestamp").notNull().primaryKey();
|
||||
workoutDataSample.addByteProperty("heartRate").notNull();
|
||||
workoutDataSample.addShortProperty("speed").notNull();
|
||||
workoutDataSample.addByteProperty("stepRate").notNull();
|
||||
workoutDataSample.addShortProperty("cadence").notNull();
|
||||
workoutDataSample.addShortProperty("stepLength").notNull();
|
||||
workoutDataSample.addShortProperty("groundContactTime").notNull();
|
||||
workoutDataSample.addByteProperty("impact").notNull();
|
||||
workoutDataSample.addShortProperty("swingAngle").notNull();
|
||||
workoutDataSample.addByteProperty("foreFootLanding").notNull();
|
||||
workoutDataSample.addByteProperty("midFootLanding").notNull();
|
||||
workoutDataSample.addByteProperty("backFootLanding").notNull();
|
||||
workoutDataSample.addByteProperty("eversionAngle").notNull();
|
||||
workoutDataSample.addByteProperty("swolf").notNull();
|
||||
workoutDataSample.addShortProperty("strokeRate").notNull();
|
||||
|
||||
workoutDataSample.addByteArrayProperty("dataErrorHex");
|
||||
|
||||
return workoutDataSample;
|
||||
}
|
||||
|
||||
private static Entity addHuaweiWorkoutPaceSample(Schema schema, Entity user, Entity device, Entity summaryEntity) {
|
||||
Entity workoutPaceSample = addEntity(schema, "HuaweiWorkoutPaceSample");
|
||||
|
||||
workoutPaceSample.setJavaDoc("Contains Huawei Workout pace data samples (one per workout)");
|
||||
|
||||
Property id = workoutPaceSample.addLongProperty("workoutId").primaryKey().notNull().getProperty();
|
||||
workoutPaceSample.addToOne(summaryEntity, id);
|
||||
|
||||
workoutPaceSample.addIntProperty("distance").notNull().primaryKey();
|
||||
workoutPaceSample.addByteProperty("type").notNull().primaryKey();
|
||||
workoutPaceSample.addIntProperty("pace").notNull();
|
||||
workoutPaceSample.addIntProperty("correction").notNull();
|
||||
|
||||
return workoutPaceSample;
|
||||
}
|
||||
|
||||
private static void addTemperatureProperties(Entity activitySample) {
|
||||
activitySample.addFloatProperty(SAMPLE_TEMPERATURE).notNull().codeBeforeGetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_TEMPERATURE_TYPE).notNull().codeBeforeGetter(OVERRIDE);
|
||||
@ -1022,5 +1122,4 @@ public class GBDaoGenerator {
|
||||
addTemperatureProperties(sample);
|
||||
return sample;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -197,10 +197,19 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_DO_NOT_DISTURB_START = "do_not_disturb_start";
|
||||
public static final String PREF_DO_NOT_DISTURB_END = "do_not_disturb_end";
|
||||
public static final String PREF_DO_NOT_DISTURB_LIFT_WRIST = "do_not_disturb_lift_wrist";
|
||||
public static final String PREF_DO_NOT_DISTURB_NOT_WEAR = "do_not_disturb_not_wear";
|
||||
public static final String PREF_DO_NOT_DISTURB_OFF = "off";
|
||||
public static final String PREF_DO_NOT_DISTURB_AUTOMATIC = "automatic";
|
||||
public static final String PREF_DO_NOT_DISTURB_ALWAYS = "always";
|
||||
public static final String PREF_DO_NOT_DISTURB_SCHEDULED = "scheduled";
|
||||
public static final String PREF_DO_NOT_DISTURB_MO = "pref_do_not_disturb_mo";
|
||||
public static final String PREF_DO_NOT_DISTURB_TU = "pref_do_not_disturb_tu";
|
||||
public static final String PREF_DO_NOT_DISTURB_WE = "pref_do_not_disturb_we";
|
||||
public static final String PREF_DO_NOT_DISTURB_TH = "pref_do_not_disturb_th";
|
||||
public static final String PREF_DO_NOT_DISTURB_FR = "pref_do_not_disturb_fr";
|
||||
public static final String PREF_DO_NOT_DISTURB_SA = "pref_do_not_disturb_sa";
|
||||
public static final String PREF_DO_NOT_DISTURB_SU = "pref_do_not_disturb_su";
|
||||
|
||||
public static final String PREF_CAMERA_REMOTE = "camera_remote";
|
||||
|
||||
public static final String PREF_WORKOUT_START_ON_PHONE = "workout_start_on_phone";
|
||||
@ -387,6 +396,19 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_VOICE_SERVICE_LANGUAGE = "voice_service_language";
|
||||
|
||||
public static final String PREF_TEMPERATURE_SCALE_CF = "temperature_scale_cf";
|
||||
|
||||
public static final String PREF_FAKE_ANDROID_ID = "fake_android_id";
|
||||
|
||||
public static final String PREF_HEARTRATE_AUTOMATIC_ENABLE = "heartrate_automatic_enable";
|
||||
public static final String PREF_SPO_AUTOMATIC_ENABLE = "spo_automatic_enable";
|
||||
|
||||
public static final String PREF_FORCE_OPTIONS = "pref_force_options";
|
||||
public static final String PREF_FORCE_ENABLE_SMART_ALARM = "pref_force_enable_smart_alarm";
|
||||
public static final String PREF_FORCE_ENABLE_WEAR_LOCATION = "pref_force_enable_wear_location";
|
||||
public static final String PREF_FORCE_DND_SUPPORT = "pref_force_dnd_support";
|
||||
public static final String PREF_IGNORE_WAKEUP_STATUS_START = "pref_force_ignore_wakeup_status_start";
|
||||
public static final String PREF_IGNORE_WAKEUP_STATUS_END = "pref_force_ignore_wakeup_status_end";
|
||||
|
||||
public static final String PREF_FEMOMETER_MEASUREMENT_MODE = "femometer_measurement_mode";
|
||||
|
||||
public static final String PREF_PREFIX_NOTIFICATION_WITH_APP = "pref_prefix_notification_with_app";
|
||||
|
@ -392,7 +392,18 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_START);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_END);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_START);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_END);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_MO);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_TU);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_WE);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_TH);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_FR);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_SA);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_SU);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_LIFT_WRIST);
|
||||
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOT_WEAR);
|
||||
addPreferenceHandlerFor(PREF_FIND_PHONE);
|
||||
addPreferenceHandlerFor(PREF_FIND_PHONE_DURATION);
|
||||
addPreferenceHandlerFor(PREF_AUTOLIGHT);
|
||||
@ -569,6 +580,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
addPreferenceHandlerFor(PREF_CLAP_HANDS_TO_WAKEUP_DEVICE);
|
||||
addPreferenceHandlerFor(PREF_POWER_SAVING);
|
||||
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_AUTOMATIC_ENABLE);
|
||||
addPreferenceHandlerFor(PREF_SPO_AUTOMATIC_ENABLE);
|
||||
|
||||
addPreferenceHandlerFor("lock");
|
||||
|
||||
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
||||
|
@ -0,0 +1,225 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiBRSupport;
|
||||
|
||||
public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordinator implements HuaweiCoordinatorSupplier {
|
||||
|
||||
private final HuaweiCoordinator huaweiCoordinator = new HuaweiCoordinator(this);
|
||||
private GBDevice device;
|
||||
|
||||
@Override
|
||||
public HuaweiCoordinator getHuaweiCoordinator() {
|
||||
return huaweiCoordinator;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
ParcelUuid huaweiService = new ParcelUuid(HuaweiConstants.UUID_SERVICE_HUAWEI_SERVICE);
|
||||
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(huaweiService).build();
|
||||
return Collections.singletonList(filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
|
||||
return new HuaweiSettingsCustomizer(device);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(){
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
long deviceId = device.getId();
|
||||
QueryBuilder<?> qb = session.getHuaweiActivitySampleDao().queryBuilder();
|
||||
qb.where(HuaweiActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
QueryBuilder<HuaweiWorkoutSummarySample> qb2 = session.getHuaweiWorkoutSummarySampleDao().queryBuilder();
|
||||
List<HuaweiWorkoutSummarySample> workouts = qb2.where(HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId)).build().list();
|
||||
for (HuaweiWorkoutSummarySample sample : workouts) {
|
||||
session.getHuaweiWorkoutDataSampleDao().queryBuilder().where(
|
||||
HuaweiWorkoutDataSampleDao.Properties.WorkoutId.eq(sample.getWorkoutId())
|
||||
).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
session.getHuaweiWorkoutSummarySampleDao().queryBuilder().where(HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
session.getBaseActivitySummaryDao().queryBuilder().where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Huawei";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return huaweiCoordinator.supportsSmartAlarm(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmSnoozing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmDescription(GBDevice device) {
|
||||
// TODO: only name is supported
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount(GBDevice device) {
|
||||
return huaweiCoordinator.getAlarmSlotCount(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMusicInfo() {
|
||||
return getHuaweiCoordinator().supportsMusic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSampleProvider(device, session);
|
||||
}
|
||||
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{};
|
||||
}
|
||||
|
||||
@Override
|
||||
public HuaweiDeviceType getHuaweiType() {
|
||||
return HuaweiDeviceType.BR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDevice(GBDevice device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice getDevice() {
|
||||
return this.device;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return HuaweiBRSupport.class;
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
|
||||
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.huawei;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID;
|
||||
|
||||
public final class HuaweiConstants {
|
||||
|
||||
public static final UUID UUID_SERVICE_HUAWEI_SERVICE = UUID.fromString(String.format(BASE_UUID, "FE86"));
|
||||
public static final UUID UUID_CHARACTERISTIC_HUAWEI_WRITE = UUID.fromString(String.format(BASE_UUID, "FE01"));
|
||||
public static final UUID UUID_CHARACTERISTIC_HUAWEI_READ = UUID.fromString(String.format(BASE_UUID, "FE02"));
|
||||
public static final UUID UUID_SERVICE_HUAWEI_SDP = UUID.fromString("82FF3820-8411-400C-B85A-55BDB32CF060");
|
||||
|
||||
public static final String GROUP_ID = "7B0BC0CBCE474F6C238D9661C63400B797B166EA7849B3A370FC73A9A236E989";
|
||||
public static final byte[] KEY_TYPE = new byte[]{0x00, 0x07};
|
||||
|
||||
public static final byte HUAWEI_MAGIC = 0x5A;
|
||||
|
||||
public static final byte PROTOCOL_VERSION = 0x02;
|
||||
|
||||
public static final int TAG_RESULT = 127;
|
||||
public static final byte[] RESULT_SUCCESS = new byte[]{0x00, 0x01, (byte)0x86, (byte)0xA0};
|
||||
|
||||
public static class CryptoTags {
|
||||
public static final int encryption = 124;
|
||||
public static final int initVector = 125;
|
||||
public static final int cipherText = 126;
|
||||
}
|
||||
|
||||
public static final String HO_BAND3_NAME = "honor band 3-";
|
||||
public static final String HO_BAND4_NAME = "honor band 4-";
|
||||
public static final String HO_BAND5_NAME = "honor band 5-";
|
||||
public static final String HO_BAND6_NAME = "honor band 6-";
|
||||
public static final String HO_BAND7_NAME = "honor band 7-";
|
||||
public static final String HU_BAND3E_NAME = "huawei band 3e-";
|
||||
public static final String HU_BAND4E_NAME = "huawei band 4e-";
|
||||
public static final String HU_BAND6_NAME = "huawei band 6-";
|
||||
public static final String HU_WATCHGT_NAME = "huawei watch gt-";
|
||||
public static final String HU_BAND4_NAME = "huawei band 4-";
|
||||
public static final String HU_BAND4PRO_NAME = "huawei band 4 pro-";
|
||||
public static final String HU_WATCHGT2_NAME = "huawei watch gt 2-";
|
||||
public static final String HU_WATCHGT2E_NAME = "huawei watch gt 2e-";
|
||||
public static final String HU_WATCHGT2PRO_NAME = "huawei watch gt 2 pro-";
|
||||
public static final String HU_TALKBANDB6_NAME = "huawei b6-";
|
||||
public static final String HU_BAND7_NAME = "huawei band 7-";
|
||||
public static final String HU_BAND8_NAME = "huawei band 8-";
|
||||
public static final String HU_WATCHGT3_NAME = "huawei watch gt 3-";
|
||||
public static final String HU_WATCHGT3PRO_NAME = "huawei watch gt 3 pro-";
|
||||
|
||||
public static final String PREF_HUAWEI_ADDRESS = "huawei_address";
|
||||
public static final String PREF_HUAWEI_WORKMODE = "workmode";
|
||||
public static final String PREF_HUAWEI_TRUSLEEP = "trusleep";
|
||||
public static final String PREF_HUAWEI_DND_LIFT_WRIST_TYPE = "dnd_lift_wrist_type"; // SharedPref for 0x01 0x1D
|
||||
public static final String PREF_HUAWEI_DEBUG = "debug_huawei";
|
||||
public static final String PREF_HUAWEI_DEBUG_REQUEST = "debug_huawei_request";
|
||||
|
||||
public static final String PKG_NAME = "com.huawei.devicegroupmanage";
|
||||
}
|
@ -0,0 +1,429 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications.NotificationConstraintsType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
|
||||
public class HuaweiCoordinator {
|
||||
Logger LOG = LoggerFactory.getLogger(HuaweiCoordinator.class);
|
||||
|
||||
TreeMap<Integer, byte[]> commandsPerService = new TreeMap<>();
|
||||
// Each byte of expandCapabilities represent a "service"
|
||||
// Each bit in a "service" represent a feature so 1 or 0 is used to check is support or not
|
||||
byte[] expandCapabilities = null;
|
||||
byte notificationCapabilities = -0x01;
|
||||
ByteBuffer notificationConstraints = null;
|
||||
|
||||
private final HuaweiCoordinatorSupplier parent;
|
||||
private boolean transactionCrypted=true;
|
||||
|
||||
public HuaweiCoordinator(HuaweiCoordinatorSupplier parent) {
|
||||
this.parent = parent;
|
||||
for (String key : getCapabilitiesSharedPreferences().getAll().keySet()) {
|
||||
int service;
|
||||
try {
|
||||
service = Integer.parseInt(key);
|
||||
byte[] commands = GB.hexStringToByteArray(getCapabilitiesSharedPreferences().getString(key, "00"));
|
||||
this.commandsPerService.put(service, commands);
|
||||
} catch (NumberFormatException e) {
|
||||
if (key == "expandCapabilities")
|
||||
this.expandCapabilities = GB.hexStringToByteArray(getCapabilitiesSharedPreferences().getString(key, "00"));
|
||||
if (key == "notificationCapabilities")
|
||||
this.notificationCapabilities = (byte)getCapabilitiesSharedPreferences().getInt(key, -0x01);
|
||||
if (key == "notificationConstraints")
|
||||
this.notificationConstraints = ByteBuffer.wrap(GB.hexStringToByteArray(
|
||||
getCapabilitiesSharedPreferences().getString(
|
||||
key,
|
||||
"00F00002001E0002001E0002001E")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SharedPreferences getCapabilitiesSharedPreferences() {
|
||||
return GBApplication.getContext().getSharedPreferences("huawei_coordinator_capatilities" + parent.getDeviceType().name(), Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private SharedPreferences getDeviceSpecificSharedPreferences(GBDevice gbDevice) {
|
||||
return GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
}
|
||||
|
||||
public boolean getForceOption(GBDevice gbDevice, String option) {
|
||||
return getDeviceSpecificSharedPreferences(gbDevice).getBoolean(option, false);
|
||||
}
|
||||
|
||||
private void saveCommandsForService(int service, byte[] commands) {
|
||||
commandsPerService.put(service, commands);
|
||||
getCapabilitiesSharedPreferences().edit().putString(String.valueOf(service), GB.hexdump(commands)).apply();
|
||||
}
|
||||
|
||||
public void saveExpandCapabilities(byte[] capabilities) {
|
||||
expandCapabilities = capabilities;
|
||||
getCapabilitiesSharedPreferences().edit().putString("expandCapabilities", GB.hexdump(capabilities)).apply();
|
||||
}
|
||||
|
||||
public void saveNotificationCapabilities(byte capabilities) {
|
||||
notificationCapabilities = capabilities;
|
||||
getCapabilitiesSharedPreferences().edit().putInt("notificationCapabilities", (int)capabilities).apply();
|
||||
}
|
||||
|
||||
public void saveNotificationConstraints(ByteBuffer constraints) {
|
||||
notificationConstraints = constraints;
|
||||
getCapabilitiesSharedPreferences().edit().putString("notificationConstraints", GB.hexdump(constraints.array())).apply();
|
||||
}
|
||||
|
||||
public void addCommandsForService(int service, byte[] commands) {
|
||||
if (!commandsPerService.containsKey(service)) {
|
||||
saveCommandsForService(service, commands);
|
||||
return;
|
||||
}
|
||||
byte[] saved = commandsPerService.get(service);
|
||||
if (saved == null) {
|
||||
saveCommandsForService(service, commands);
|
||||
return;
|
||||
}
|
||||
if (saved.length != commands.length) {
|
||||
saveCommandsForService(service, commands);
|
||||
return;
|
||||
}
|
||||
boolean changed = false;
|
||||
for (int i = 0; i < saved.length; i++) {
|
||||
if (saved[i] != commands[i]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
saveCommandsForService(service, commands);
|
||||
}
|
||||
|
||||
public byte[] getCommandsForService(int service) {
|
||||
return commandsPerService.get(service);
|
||||
}
|
||||
|
||||
// Print all Services ID and Commands ID
|
||||
public void printCommandsPerService() {
|
||||
StringBuilder msg = new StringBuilder();
|
||||
for(Map.Entry<Integer, byte[]> entry : commandsPerService.entrySet()) {
|
||||
msg.append("ServiceID: ").append(entry.getKey()).append(" => Commands: ");
|
||||
for (byte b: entry.getValue()) {
|
||||
msg.append(Integer.toHexString(b)).append(" ");
|
||||
}
|
||||
msg.append("\n");
|
||||
}
|
||||
LOG.info(msg.toString());
|
||||
}
|
||||
|
||||
private boolean supportsCommandForService(int service, int command) {
|
||||
byte[] commands = commandsPerService.get(service);
|
||||
if (commands == null)
|
||||
return false;
|
||||
for (byte b : commands)
|
||||
if (b == (byte) command)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean supportsExpandCapability(int which) {
|
||||
// capability is a number containing :
|
||||
// - the index of the "service"
|
||||
// - the real capability number
|
||||
if (which >= expandCapabilities.length * 8) {
|
||||
LOG.debug("Capability is not supported");
|
||||
return false;
|
||||
}
|
||||
int capability = 1 << (which % 8);
|
||||
if ((expandCapabilities[which / 8] & capability) == capability) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean supportsNotificationConstraint(byte which) {
|
||||
return notificationConstraints.get(which) == 0x01;
|
||||
}
|
||||
|
||||
private int getNotificationConstraint(byte which) {
|
||||
return notificationConstraints.get(which);
|
||||
}
|
||||
|
||||
public int[] genericHuaweiSupportedDeviceSpecificSettings(int[] additionalDeviceSpecificSettings) {
|
||||
// Add all settings in the default table
|
||||
// Hide / show table in HuaweiSettingsCustommizer
|
||||
List<Integer> dynamicSupportedDeviceSpecificSettings = new ArrayList<>();
|
||||
|
||||
// Could be limited to 0x04 0x01, but I don't know if that'll work properly
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_allow_accept_reject_calls);
|
||||
|
||||
// Only supported on specific devices
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_huawei);
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_trusleep);
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_wearlocation);
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_dateformat);
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_timeformat);
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_workmode);
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_liftwrist_display_noshed);
|
||||
dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_rotatewrist_cycleinfo);
|
||||
|
||||
int size = dynamicSupportedDeviceSpecificSettings.size();
|
||||
if (additionalDeviceSpecificSettings != null)
|
||||
size += additionalDeviceSpecificSettings.length;
|
||||
int[] result = new int[size];
|
||||
|
||||
for (int i = 0; i < dynamicSupportedDeviceSpecificSettings.size(); i++)
|
||||
result[i] = dynamicSupportedDeviceSpecificSettings.get(i);
|
||||
|
||||
if (additionalDeviceSpecificSettings != null)
|
||||
System.arraycopy(additionalDeviceSpecificSettings, 0, result, dynamicSupportedDeviceSpecificSettings.size(), additionalDeviceSpecificSettings.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean supportsDateFormat() {
|
||||
return supportsCommandForService(0x01, 0x04);
|
||||
}
|
||||
|
||||
public boolean supportsActivateOnLift() {
|
||||
return supportsCommandForService(0x01, 0x09);
|
||||
}
|
||||
|
||||
public boolean supportsDoNotDisturb() {
|
||||
return supportsCommandForService(0x01, 0x0a);
|
||||
}
|
||||
public boolean supportsDoNotDisturb(GBDevice gbDevice) {
|
||||
return supportsDoNotDisturb() || getForceOption(gbDevice, PREF_FORCE_DND_SUPPORT);
|
||||
}
|
||||
|
||||
public boolean supportsActivityType() {
|
||||
return supportsCommandForService(0x01, 0x12);
|
||||
}
|
||||
|
||||
public boolean supportsWearLocation() {
|
||||
return supportsCommandForService(0x01, 0x1a);
|
||||
}
|
||||
public boolean supportsWearLocation(GBDevice gbDevice) {
|
||||
return supportsWearLocation() || getForceOption(gbDevice, PREF_FORCE_ENABLE_WEAR_LOCATION);
|
||||
}
|
||||
|
||||
public boolean supportsRotateToCycleInfo() {
|
||||
return supportsCommandForService(0x01, 0x1b);
|
||||
}
|
||||
|
||||
public boolean supportsQueryDndLiftWristDisturbType() {
|
||||
return supportsCommandForService(0x01, 0x1d);
|
||||
}
|
||||
|
||||
public boolean supportsSettingRelated() {
|
||||
return supportsCommandForService(0x01, 0x31);
|
||||
}
|
||||
|
||||
public boolean supportsTimeAndZoneId() {
|
||||
return supportsCommandForService(0x01, 0x32);
|
||||
}
|
||||
|
||||
public boolean supportsConnectStatus() {
|
||||
return supportsCommandForService(0x01, 0x35);
|
||||
}
|
||||
|
||||
public boolean supportsExpandCapability() {
|
||||
return supportsCommandForService(0x01, 0x37);
|
||||
}
|
||||
|
||||
public boolean supportsNotificationAlert() {
|
||||
return supportsCommandForService(0x02, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsNotification() {
|
||||
return supportsCommandForService(0x02, 0x04);
|
||||
}
|
||||
|
||||
public boolean supportsWearMessagePush() {
|
||||
return supportsCommandForService(0x02, 0x08);
|
||||
}
|
||||
|
||||
|
||||
public boolean supportsMotionGoal() {
|
||||
return supportsCommandForService(0x07, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsInactivityWarnings() {
|
||||
return supportsCommandForService(0x07, 0x06);
|
||||
}
|
||||
|
||||
public boolean supportsActivityReminder() {
|
||||
return supportsCommandForService(0x07, 0x07);
|
||||
}
|
||||
|
||||
public boolean supportsTruSleep() {
|
||||
return supportsCommandForService(0x07, 0x16);
|
||||
}
|
||||
|
||||
public boolean supportsHeartRate() {
|
||||
// TODO: this is not correct
|
||||
return supportsCommandForService(0x07, 0x17);
|
||||
}
|
||||
|
||||
public boolean supportsFitnessRestHeartRate() {
|
||||
return supportsCommandForService(0x07, 0x23);
|
||||
}
|
||||
|
||||
public boolean supportsFitnessThresholdValue() {
|
||||
return supportsCommandForService(0x07, 0x29);
|
||||
}
|
||||
|
||||
public boolean supportsEventAlarm() {
|
||||
return supportsCommandForService(0x08, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsSmartAlarm() {
|
||||
return supportsCommandForService(0x08, 0x02) ;
|
||||
}
|
||||
public boolean supportsSmartAlarm(GBDevice gbDevice) {
|
||||
return supportsSmartAlarm() || getForceOption(gbDevice, PREF_FORCE_ENABLE_SMART_ALARM);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if alarms can be changed on the device, false otherwise
|
||||
*/
|
||||
public boolean supportsChangingAlarm() {
|
||||
return supportsCommandForService(0x08, 0x03);
|
||||
}
|
||||
|
||||
public boolean supportsNotificationOnBluetoothLoss() {
|
||||
return supportsCommandForService(0x0b, 0x03);
|
||||
}
|
||||
|
||||
public boolean supportsLanguageSetting() {
|
||||
return supportsCommandForService(0x0c, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsWorkouts() {
|
||||
return supportsCommandForService(0x17, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsWorkoutsTrustHeartRate() {
|
||||
return supportsCommandForService(0x17, 0x17);
|
||||
}
|
||||
|
||||
public boolean supportsAccount() {
|
||||
return supportsCommandForService(0x1A, 0x05) || supportsCommandForService(0x1A, 0x06);
|
||||
}
|
||||
|
||||
public boolean supportsMusic() {
|
||||
return supportsCommandForService(0x25, 0x02);
|
||||
}
|
||||
|
||||
public boolean supportsAutoWorkMode() {
|
||||
return supportsCommandForService(0x26, 0x02);
|
||||
}
|
||||
|
||||
public boolean supportsMenstrual() {
|
||||
return supportsCommandForService(0x32, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsMultiDevice() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(109);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsPromptPushMessage () {
|
||||
// do not ask for capabilities under specific condition
|
||||
// if (deviceType == 10 && deviceVersion == 73617766697368 && deviceSoftVersion == 372E312E31) -> leo device
|
||||
// if V1V0Device
|
||||
// if (serviceId = 0x01 && commandId = 0x03) && productType == 3
|
||||
return (((notificationCapabilities >> 1) & 1) == 0);
|
||||
}
|
||||
|
||||
public boolean supportsOutgoingCall () {
|
||||
return (((notificationCapabilities >> 2) & 1) == 0);
|
||||
}
|
||||
|
||||
public boolean supportsYellowPages() {
|
||||
return supportsNotificationConstraint(NotificationConstraintsType.yellowPagesSupport);
|
||||
}
|
||||
|
||||
public boolean supportsContentSIgn() {
|
||||
return supportsNotificationConstraint(NotificationConstraintsType.contentSignSupport);
|
||||
}
|
||||
|
||||
public boolean supportsIncomingNumber() {
|
||||
return supportsNotificationConstraint(NotificationConstraintsType.incomingNumberSupport);
|
||||
}
|
||||
|
||||
public byte getYellowPagesFormat() {
|
||||
return (byte)getNotificationConstraint(NotificationConstraintsType.yellowPagesFormat);
|
||||
}
|
||||
|
||||
public byte getContentSignFormat() {
|
||||
return (byte)getNotificationConstraint(NotificationConstraintsType.contentSignFormat);
|
||||
}
|
||||
|
||||
public byte getIncomingFormatFormat() {
|
||||
return (byte)getNotificationConstraint(NotificationConstraintsType.incomingNumberFormat);
|
||||
}
|
||||
|
||||
public short getContentLength() {
|
||||
return (short)getNotificationConstraint(NotificationConstraintsType.contentLength);
|
||||
}
|
||||
|
||||
public short getYellowPagesLength() {
|
||||
return (short)getNotificationConstraint(NotificationConstraintsType.yellowPagesLength);
|
||||
}
|
||||
|
||||
public short getContentSignLength() {
|
||||
return (short)getNotificationConstraint(NotificationConstraintsType.contentSignLength);
|
||||
}
|
||||
|
||||
public short getIncomingNumberLength() {
|
||||
return (short)getNotificationConstraint(NotificationConstraintsType.incomingNumberLength);
|
||||
}
|
||||
|
||||
public int getAlarmSlotCount(GBDevice gbDevice) {
|
||||
int alarmCount = 0;
|
||||
if (supportsEventAlarm())
|
||||
alarmCount += 5; // Always five event alarms
|
||||
if (supportsSmartAlarm(gbDevice))
|
||||
alarmCount += 1; // Always a single smart alarm
|
||||
return alarmCount;
|
||||
}
|
||||
|
||||
public void setTransactionCrypted(boolean crypted) {
|
||||
this.transactionCrypted = crypted;
|
||||
}
|
||||
|
||||
public boolean isTransactionCrypted() {
|
||||
return this.transactionCrypted;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public interface HuaweiCoordinatorSupplier {
|
||||
|
||||
enum HuaweiDeviceType {
|
||||
AW(0), //BLE behind
|
||||
BR(1),
|
||||
BLE(2),
|
||||
SMART(5) //BLE behind
|
||||
;
|
||||
|
||||
final int huaweiType;
|
||||
|
||||
HuaweiDeviceType(int huaweiType) {
|
||||
this.huaweiType = huaweiType;
|
||||
}
|
||||
|
||||
public int getType(){
|
||||
return huaweiType;
|
||||
}
|
||||
}
|
||||
|
||||
HuaweiCoordinator getHuaweiCoordinator();
|
||||
HuaweiDeviceType getHuaweiType();
|
||||
DeviceType getDeviceType();
|
||||
void setDevice(GBDevice Device);
|
||||
GBDevice getDevice();
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class HuaweiCrypto {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiCrypto.class);
|
||||
|
||||
public static class CryptoException extends Exception {
|
||||
CryptoException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final byte[] SECRET_KEY_1_v1 = new byte[]{ 0x6F, 0x75, 0x6A, 0x79,
|
||||
0x6D, 0x77, 0x71, 0x34,
|
||||
0x63, 0x6C, 0x76, 0x39,
|
||||
0x33, 0x37, 0x38, 0x79};
|
||||
public static final byte[] SECRET_KEY_2_v1 = new byte[]{ 0x62, 0x31, 0x30, 0x6A,
|
||||
0x67, 0x66, 0x64, 0x39,
|
||||
0x79, 0x37, 0x76, 0x73,
|
||||
0x75, 0x64, 0x61, 0x39};
|
||||
public static final byte[] SECRET_KEY_1_v23 = new byte[]{ 0x55, 0x53, (byte)0x86, (byte)0xFC,
|
||||
0x63, 0x20, 0x07, (byte)0xAA,
|
||||
(byte)0x86, 0x49, 0x35, 0x22,
|
||||
(byte)0xB8, 0x6A, (byte)0xE2, 0x5C};
|
||||
public static final byte[] SECRET_KEY_2_v23 = new byte[]{ 0x33, 0x07, (byte)0x9B, (byte)0xC5,
|
||||
0x7A, (byte)0x88, 0x6D, 0x3C,
|
||||
(byte)0xF5, 0x61, 0x37, 0x09,
|
||||
0x6F, 0x22, (byte)0x80, 0x00};
|
||||
|
||||
public static final byte[] DIGEST_SECRET_v1 = new byte[]{ 0x70, (byte)0xFB, 0x6C, 0x24,
|
||||
0x03, 0x5F, (byte)0xDB, 0x55,
|
||||
0x2F, 0x38, (byte)0x89, (byte)0x8A,
|
||||
(byte) 0xEE, (byte)0xDE, 0x3F, 0x69};
|
||||
public static final byte[] DIGEST_SECRET_v2 = new byte[]{ (byte)0x93, (byte)0xAC, (byte)0xDE, (byte)0xF7,
|
||||
0x6A, (byte)0xCB, 0x09, (byte)0x85,
|
||||
0x7D, (byte)0xBF, (byte)0xE5, 0x26,
|
||||
0x1A, (byte)0xAB, (byte)0xCD, 0x78};
|
||||
public static final byte[] DIGEST_SECRET_v3 = new byte[]{ (byte)0x9C, 0x27, 0x63, (byte)0xA9,
|
||||
(byte)0xCC, (byte)0xE1, 0x34, 0x76,
|
||||
0x6D, (byte)0xE3, (byte)0xFF, 0x61,
|
||||
0x18, 0x20, 0x05, 0x53};
|
||||
|
||||
public static final byte[] MESSAGE_RESPONSE = new byte[]{0x01, 0x10};
|
||||
public static final byte[] MESSAGE_CHALLENGE = new byte[]{0x01, 0x00};
|
||||
|
||||
public static final long ENCRYPTION_COUNTER_MAX = 0xFFFFFFFF;
|
||||
|
||||
protected int authVersion;
|
||||
protected boolean isHiChainLite = false;
|
||||
|
||||
public HuaweiCrypto(int authVersion) {
|
||||
this.authVersion = authVersion;
|
||||
}
|
||||
|
||||
public HuaweiCrypto(int authVersion, boolean isHiChainLite) {
|
||||
this(authVersion);
|
||||
this.isHiChainLite = isHiChainLite;
|
||||
}
|
||||
|
||||
public static byte[] generateNonce() {
|
||||
// While technically not a nonce, we need it to be random and rely on the length for the chance of repitition to be small
|
||||
byte[] returnValue = new byte[16];
|
||||
(new SecureRandom()).nextBytes(returnValue);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private byte[] getDigestSecret() {
|
||||
if (authVersion == 1) {
|
||||
return DIGEST_SECRET_v1;
|
||||
} else if (authVersion == 2) {
|
||||
return DIGEST_SECRET_v2;
|
||||
} else {
|
||||
return DIGEST_SECRET_v3;
|
||||
}
|
||||
}
|
||||
public byte[] computeDigest(byte[] message, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
byte[] digestSecret = getDigestSecret();
|
||||
byte[] msgToDigest = ByteBuffer.allocate(16 + message.length)
|
||||
.put(digestSecret)
|
||||
.put(message)
|
||||
.array();
|
||||
byte[] digestStep1 = CryptoUtils.calcHmacSha256(msgToDigest, nonce);
|
||||
return CryptoUtils.calcHmacSha256(digestStep1, nonce);
|
||||
}
|
||||
|
||||
public byte[] computeDigestHiChainLite(byte[] message, byte[] key, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
byte[] hashKey = CryptoUtils.digest(key);
|
||||
byte[] digestSecret = getDigestSecret();
|
||||
for (int i = 0; i < digestSecret.length; i++) {
|
||||
digestSecret[i] = (byte) (((0xFF & hashKey[i]) ^ (digestSecret[i] & 0xFF)) & 0xFF);
|
||||
}
|
||||
// 2 possibilities:
|
||||
// - type 1 : Pbk (SDK_INT>= 0x17) fallback to MacSha
|
||||
// - type 2 : MacSha
|
||||
// We force type 2 to avoid a new calculation
|
||||
byte[] msgToDigest = ByteBuffer.allocate(18)
|
||||
.put(digestSecret)
|
||||
.put(message)
|
||||
.array();
|
||||
byte[] digestStep1 = CryptoUtils.calcHmacSha256(msgToDigest, nonce) ;
|
||||
return CryptoUtils.calcHmacSha256(digestStep1, nonce);
|
||||
}
|
||||
|
||||
public byte[] digestChallenge(byte[] secretKey, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (isHiChainLite) {
|
||||
if (secretKey == null)
|
||||
return null;
|
||||
if (authVersion == 0x02) {
|
||||
byte[] key = ByteBuffer.allocate(18)
|
||||
.put(secretKey)
|
||||
.put(MESSAGE_CHALLENGE)
|
||||
.array();
|
||||
return CryptoUtils.calcHmacSha256(key, nonce);
|
||||
}
|
||||
return computeDigestHiChainLite(MESSAGE_CHALLENGE, secretKey, nonce);
|
||||
}
|
||||
return computeDigest(MESSAGE_CHALLENGE, nonce);
|
||||
}
|
||||
|
||||
public byte[] digestResponse(byte[] secretKey, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (isHiChainLite) {
|
||||
if (secretKey == null)
|
||||
return null;
|
||||
if (authVersion == 0x02) {
|
||||
byte[] key = ByteBuffer.allocate(18)
|
||||
.put(secretKey)
|
||||
.put(MESSAGE_RESPONSE)
|
||||
.array();
|
||||
return CryptoUtils.calcHmacSha256(key, nonce);
|
||||
}
|
||||
return computeDigestHiChainLite(MESSAGE_RESPONSE, secretKey, nonce);
|
||||
}
|
||||
return computeDigest(MESSAGE_RESPONSE, nonce);
|
||||
}
|
||||
|
||||
public static ByteBuffer initializationVector(long counter) {
|
||||
if (counter == ENCRYPTION_COUNTER_MAX) {
|
||||
counter = 1;
|
||||
} else {
|
||||
counter += 1;
|
||||
}
|
||||
ByteBuffer ivCounter = ByteBuffer.allocate(16);
|
||||
ivCounter.put(generateNonce(), 0, 12);
|
||||
ivCounter.put(ByteBuffer.allocate(8).putLong(counter).array(), 4, 4);
|
||||
ivCounter.rewind();
|
||||
return ivCounter;
|
||||
}
|
||||
|
||||
public byte[] createSecretKey(String macAddress) throws NoSuchAlgorithmException {
|
||||
byte[] secret_key_1 = SECRET_KEY_1_v23;
|
||||
byte[] secret_key_2 = SECRET_KEY_2_v23;
|
||||
if (authVersion == 1) {
|
||||
secret_key_1 = SECRET_KEY_1_v1;
|
||||
secret_key_2 = SECRET_KEY_2_v1;
|
||||
}
|
||||
|
||||
byte[] macAddressKey = (macAddress.replace(":", "") + "0000").getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] mixedSecretKey = new byte[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
mixedSecretKey[i] = (byte)((((0xFF & secret_key_1[i]) << 4) ^ (0xFF & secret_key_2[i])) & 0xFF);
|
||||
}
|
||||
|
||||
byte[] mixedSecretKeyHash = CryptoUtils.digest(mixedSecretKey);
|
||||
byte[] finalMixedKey = new byte[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
finalMixedKey[i] = (byte)((((0xFF & mixedSecretKeyHash[i]) >> 6) ^ (0xFF & macAddressKey[i])) & 0xFF);
|
||||
}
|
||||
byte[] finalMixedKeyHash = CryptoUtils.digest(finalMixedKey);
|
||||
return Arrays.copyOfRange(finalMixedKeyHash, 0, 16);
|
||||
}
|
||||
|
||||
public byte[] encryptBondingKey(byte[] data, String mac, byte[] iv) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IllegalArgumentException {
|
||||
byte[] encryptionKey = createSecretKey(mac);
|
||||
return CryptoUtils.encryptAES_CBC_Pad(data, encryptionKey, iv);
|
||||
}
|
||||
|
||||
public byte[] decryptBondingKey(byte[] data, String mac, byte[] iv) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IllegalArgumentException {
|
||||
byte[] encryptionKey = createSecretKey(mac);
|
||||
return CryptoUtils.decryptAES_CBC_Pad(data, encryptionKey, iv);
|
||||
}
|
||||
|
||||
public byte[] decryptPinCode(byte[] message, byte[] iv) throws CryptoException {
|
||||
byte[] secretKey = getDigestSecret();
|
||||
try {
|
||||
return CryptoUtils.decryptAES_CBC_Pad(message, secretKey, iv);
|
||||
} catch (Exception e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] encrypt(byte authMode, byte[] message, byte[] key, byte[] iv) throws CryptoException {
|
||||
try {
|
||||
if (authMode == 0x04) {
|
||||
return CryptoUtils.encryptAES_GCM_NoPad(message, key, iv, null);
|
||||
} else {
|
||||
return CryptoUtils.encryptAES_CBC_Pad(message, key, iv);
|
||||
}
|
||||
} catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | IllegalArgumentException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decrypt(byte authMode, byte[] message, byte[] key, byte[] iv) throws CryptoException {
|
||||
try {
|
||||
if (authMode == 0x04) {
|
||||
return CryptoUtils.decryptAES_GCM_NoPad(message, key, iv, null);
|
||||
} else {
|
||||
return CryptoUtils.decryptAES_CBC_Pad(message, key, iv);
|
||||
}
|
||||
} catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | IllegalArgumentException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiLESupport;
|
||||
|
||||
public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator implements HuaweiCoordinatorSupplier {
|
||||
|
||||
private final HuaweiCoordinator huaweiCoordinator = new HuaweiCoordinator(this);
|
||||
private GBDevice device;
|
||||
|
||||
@Override
|
||||
public HuaweiCoordinator getHuaweiCoordinator() {
|
||||
return huaweiCoordinator;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
ParcelUuid huaweiService = new ParcelUuid(HuaweiConstants.UUID_SERVICE_HUAWEI_SERVICE);
|
||||
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(huaweiService).build();
|
||||
return Collections.singletonList(filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
|
||||
return new HuaweiSettingsCustomizer(device);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(){
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
long deviceId = device.getId();
|
||||
QueryBuilder<?> qb = session.getHuaweiActivitySampleDao().queryBuilder();
|
||||
qb.where(HuaweiActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
QueryBuilder<HuaweiWorkoutSummarySample> qb2 = session.getHuaweiWorkoutSummarySampleDao().queryBuilder();
|
||||
List<HuaweiWorkoutSummarySample> workouts = qb2.where(HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId)).build().list();
|
||||
for (HuaweiWorkoutSummarySample sample : workouts) {
|
||||
session.getHuaweiWorkoutDataSampleDao().queryBuilder().where(
|
||||
HuaweiWorkoutDataSampleDao.Properties.WorkoutId.eq(sample.getWorkoutId())
|
||||
).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
session.getHuaweiWorkoutSummarySampleDao().queryBuilder().where(HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
session.getBaseActivitySummaryDao().queryBuilder().where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Huawei";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return huaweiCoordinator.supportsSmartAlarm(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmSnoozing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmDescription(GBDevice device) {
|
||||
// TODO: only name is supported
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount(GBDevice device) {
|
||||
return huaweiCoordinator.getAlarmSlotCount(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMusicInfo() {
|
||||
return getHuaweiCoordinator().supportsMusic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSampleProvider(device, session);
|
||||
}
|
||||
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{};
|
||||
}
|
||||
|
||||
@Override
|
||||
public HuaweiDeviceType getHuaweiType() {
|
||||
return HuaweiDeviceType.BLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDevice(GBDevice device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice getDevice() {
|
||||
return this.device;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return HuaweiLESupport.class;
|
||||
}
|
||||
}
|
@ -0,0 +1,665 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.HUAWEI_MAGIC;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
|
||||
public class HuaweiPacket {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiPacket.class);
|
||||
|
||||
public static class ParamsProvider {
|
||||
protected byte authVersion;
|
||||
protected byte authMode;
|
||||
protected byte[] secretKey;
|
||||
protected int slicesize = 0xf4;
|
||||
protected boolean transactionsCrypted = true;
|
||||
protected int mtu = 65535;
|
||||
protected long encryptionCounter = 0;
|
||||
|
||||
protected byte[] pinCode = null;
|
||||
|
||||
protected byte interval;
|
||||
|
||||
public void setAuthVersion(byte authVersion) {
|
||||
this.authVersion = authVersion;
|
||||
}
|
||||
|
||||
public byte getAuthVersion() {
|
||||
return this.authVersion;
|
||||
}
|
||||
|
||||
public void setAuthMode(byte authMode) {
|
||||
this.authMode = authMode;
|
||||
}
|
||||
|
||||
public byte getAuthMode(){
|
||||
return this.authMode;
|
||||
}
|
||||
|
||||
public void setSecretKey(byte[] secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
public byte[] getSecretKey() {
|
||||
return this.secretKey;
|
||||
}
|
||||
|
||||
public void setTransactionsCrypted(boolean transactionsCrypted) {
|
||||
this.transactionsCrypted = transactionsCrypted;
|
||||
}
|
||||
|
||||
public boolean areTransactionsCrypted() {
|
||||
return this.transactionsCrypted;
|
||||
}
|
||||
|
||||
public void setMtu(int mtu) {
|
||||
this.mtu = mtu;
|
||||
}
|
||||
|
||||
public int getMtu() {
|
||||
return this.mtu;
|
||||
}
|
||||
|
||||
public void setSliceSize(int sliceSize) {
|
||||
this.slicesize = sliceSize;
|
||||
}
|
||||
|
||||
public int getSliceSize() {
|
||||
return this.slicesize;
|
||||
}
|
||||
public void setPinCode(byte[] pinCode) {
|
||||
this.pinCode = pinCode;
|
||||
}
|
||||
|
||||
public byte[] getPinCode() {
|
||||
return this.pinCode;
|
||||
}
|
||||
|
||||
public void setInterval(byte interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
public byte getInterval() {
|
||||
return this.interval;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
byte[] iv = null;
|
||||
if (this.authMode == 0x04) {
|
||||
iv = HuaweiCrypto.generateNonce();
|
||||
} else {
|
||||
ByteBuffer ivCounter = HuaweiCrypto.initializationVector(this.encryptionCounter);
|
||||
iv = ivCounter.array();
|
||||
this.encryptionCounter = (long)ivCounter.getInt(12) & 0xFFFFFFFFL;
|
||||
}
|
||||
return iv;
|
||||
}
|
||||
|
||||
public void setEncryptionCounter(long counter) {
|
||||
this.encryptionCounter = counter;
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class ParseException extends Exception {
|
||||
ParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
ParseException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LengthMismatchException extends ParseException {
|
||||
public LengthMismatchException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MagicMismatchException extends ParseException {
|
||||
MagicMismatchException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChecksumIncorrectException extends ParseException {
|
||||
ChecksumIncorrectException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MissingTagException extends ParseException {
|
||||
public MissingTagException(int tag) {
|
||||
super("Missing tag: " + Integer.toHexString(tag));
|
||||
}
|
||||
}
|
||||
|
||||
public static class CryptoException extends ParseException {
|
||||
public CryptoException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class JsonException extends ParseException {
|
||||
public JsonException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SupportedCommandsListException extends ParseException {
|
||||
public SupportedCommandsListException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SerializeException extends Exception {
|
||||
public SerializeException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected static final int PACKET_MINIMAL_SIZE = 6;
|
||||
|
||||
protected ParamsProvider paramsProvider;
|
||||
|
||||
public byte serviceId = 0;
|
||||
public byte commandId = 0;
|
||||
protected HuaweiTLV tlv = null;
|
||||
|
||||
private byte[] partialPacket = null;
|
||||
private byte[] payload = null;
|
||||
|
||||
public boolean complete = false;
|
||||
|
||||
// Encryption is enabled by default, packets which don't use it must disable it
|
||||
protected boolean isEncrypted = true;
|
||||
|
||||
protected boolean isSliced = false;
|
||||
|
||||
public HuaweiPacket(ParamsProvider paramsProvider) {
|
||||
this.paramsProvider = paramsProvider;
|
||||
}
|
||||
|
||||
public boolean attemptDecrypt() throws ParseException {
|
||||
if (this.tlv == null)
|
||||
return false;
|
||||
if (this.tlv.contains(0x7C) && this.tlv.getBoolean(0x7C)) {
|
||||
try {
|
||||
this.tlv.decrypt(paramsProvider);
|
||||
return true;
|
||||
} catch (HuaweiCrypto.CryptoException e) {
|
||||
throw new CryptoException("Decrypt exception", e);
|
||||
}
|
||||
} else {
|
||||
if (this.isEncrypted && paramsProvider.areTransactionsCrypted()) {
|
||||
// TODO: potentially a log message? We expect it to be encrypted, but it isn't.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* This function is to convert the Packet into the proper subclass
|
||||
*/
|
||||
protected HuaweiPacket fromPacket(HuaweiPacket packet) throws ParseException {
|
||||
this.paramsProvider = packet.paramsProvider;
|
||||
this.serviceId = packet.serviceId;
|
||||
this.commandId = packet.commandId;
|
||||
this.tlv = packet.tlv;
|
||||
this.partialPacket = packet.partialPacket;
|
||||
this.payload = packet.payload;
|
||||
this.complete = packet.complete;
|
||||
|
||||
if (packet.isEncrypted)
|
||||
this.isEncrypted = true;
|
||||
else
|
||||
this.isEncrypted = this.attemptDecrypt();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
* This function is to set up the subclass for easy usage
|
||||
* Needs to be called separately so the exceptions can be used more easily
|
||||
*/
|
||||
public void parseTlv() throws ParseException {}
|
||||
|
||||
private void parseData(byte[] data) throws ParseException {
|
||||
if (partialPacket != null) {
|
||||
int newCapacity = partialPacket.length + data.length;
|
||||
data = ByteBuffer.allocate(newCapacity)
|
||||
.put(partialPacket)
|
||||
.put(data)
|
||||
.array();
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
|
||||
if (buffer.capacity() < PACKET_MINIMAL_SIZE) {
|
||||
throw new LengthMismatchException("Packet length mismatch : "
|
||||
+ buffer.capacity()
|
||||
+ " != 6");
|
||||
}
|
||||
|
||||
byte magic = buffer.get();
|
||||
short expectedSize = buffer.getShort();
|
||||
int isSliced = buffer.get();
|
||||
if (isSliced == 1 || isSliced == 2 || isSliced == 3) {
|
||||
buffer.get(); // Throw away slice flag
|
||||
}
|
||||
byte[] newPayload = new byte[buffer.remaining() - 2];
|
||||
buffer.get(newPayload, 0, buffer.remaining() - 2);
|
||||
short expectedChecksum = buffer.getShort();
|
||||
buffer.rewind();
|
||||
|
||||
if (magic != HUAWEI_MAGIC) {
|
||||
throw new MagicMismatchException("Magic mismatch : "
|
||||
+ Integer.toHexString(magic)
|
||||
+ " != 0x5A");
|
||||
}
|
||||
|
||||
int newPayloadLen = newPayload.length + 1;
|
||||
if (isSliced == 1 || isSliced == 2 || isSliced == 3) {
|
||||
newPayloadLen = newPayload.length + 2;
|
||||
}
|
||||
if (expectedSize != (short) newPayloadLen) {
|
||||
if (expectedSize > (short) newPayloadLen) {
|
||||
// Older band and BT version do not handle message with more than 256 bits.
|
||||
this.partialPacket = data;
|
||||
return;
|
||||
} else {
|
||||
throw new LengthMismatchException("Expected length mismatch : "
|
||||
+ expectedSize
|
||||
+ " < "
|
||||
+ (short) newPayloadLen);
|
||||
}
|
||||
}
|
||||
this.partialPacket = null;
|
||||
|
||||
byte[] dataNoCRC = new byte[buffer.capacity() - 2];
|
||||
buffer.get(dataNoCRC, 0, buffer.capacity() - 2);
|
||||
short actualChecksum = (short) CheckSums.getCRC16(dataNoCRC, 0x0000);
|
||||
if (actualChecksum != expectedChecksum) {
|
||||
throw new ChecksumIncorrectException("Checksum mismatch : "
|
||||
+ String.valueOf(actualChecksum)
|
||||
+ " != "
|
||||
+ String.valueOf(expectedChecksum));
|
||||
}
|
||||
|
||||
if (isSliced == 1 || isSliced == 2 || isSliced == 3) {
|
||||
if (payload != null) {
|
||||
int newCapacity = payload.length + newPayload.length;
|
||||
newPayload = ByteBuffer.allocate(newCapacity)
|
||||
.put(payload)
|
||||
.put(newPayload)
|
||||
.array();
|
||||
}
|
||||
|
||||
if (isSliced != 3) {
|
||||
// Sliced packet isn't complete yet
|
||||
this.payload = newPayload;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.serviceId = newPayload[0];
|
||||
this.commandId = newPayload[1];
|
||||
this.complete = true;
|
||||
|
||||
if (
|
||||
(serviceId == 0x0a && commandId == 0x05) ||
|
||||
(serviceId == 0x28 && commandId == 0x06)
|
||||
) {
|
||||
// TODO: this doesn't seem to be TLV
|
||||
return;
|
||||
}
|
||||
|
||||
this.tlv = new HuaweiTLV();
|
||||
this.tlv.parse(newPayload, 2, newPayload.length - 2);
|
||||
}
|
||||
|
||||
public HuaweiPacket parse(byte[] data) throws ParseException {
|
||||
this.isEncrypted = false; // Will be changed if decrypt has been performed
|
||||
|
||||
parseData(data);
|
||||
if (!this.complete)
|
||||
return this;
|
||||
|
||||
switch (this.serviceId) {
|
||||
case DeviceConfig.id:
|
||||
switch (this.commandId) {
|
||||
case DeviceConfig.LinkParams.id:
|
||||
return new DeviceConfig.LinkParams.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.SupportedServices.id:
|
||||
return new DeviceConfig.SupportedServices.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.SupportedCommands.id:
|
||||
return new DeviceConfig.SupportedCommands.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.ProductInfo.id:
|
||||
return new DeviceConfig.ProductInfo.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.BondParams.id:
|
||||
return new DeviceConfig.BondParams.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.Auth.id:
|
||||
return new DeviceConfig.Auth.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.BatteryLevel.id:
|
||||
return new DeviceConfig.BatteryLevel.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.DeviceStatus.id:
|
||||
return new DeviceConfig.DeviceStatus.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.DndLiftWristType.id:
|
||||
return new DeviceConfig.DndLiftWristType.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.HiChain.id:
|
||||
return new DeviceConfig.HiChain.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.PinCode.id:
|
||||
return new DeviceConfig.PinCode.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.ExpandCapability.id:
|
||||
return new DeviceConfig.ExpandCapability.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.ActivityType.id:
|
||||
return new DeviceConfig.ActivityType.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.SettingRelated.id:
|
||||
return new DeviceConfig.SettingRelated.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.SecurityNegotiation.id:
|
||||
return new DeviceConfig.SecurityNegotiation.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.WearStatus.id:
|
||||
return new DeviceConfig.WearStatus.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case Notifications.id:
|
||||
switch (this.commandId) {
|
||||
case Notifications.NotificationConstraints.id:
|
||||
return new Notifications.NotificationConstraints.Response(paramsProvider).fromPacket(this);
|
||||
case Notifications.NotificationCapabilities.id:
|
||||
return new Notifications.NotificationCapabilities.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
case Calls.id:
|
||||
if (this.commandId == Calls.AnswerCallResponse.id)
|
||||
return new Calls.AnswerCallResponse(paramsProvider).fromPacket(this);
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
case FitnessData.id:
|
||||
switch (this.commandId) {
|
||||
case FitnessData.FitnessTotals.id:
|
||||
return new FitnessData.FitnessTotals.Response(paramsProvider).fromPacket(this);
|
||||
case FitnessData.MessageCount.stepId:
|
||||
return new FitnessData.MessageCount.Response(paramsProvider).fromPacket(this);
|
||||
case FitnessData.MessageData.stepId:
|
||||
return new FitnessData.MessageData.StepResponse(paramsProvider).fromPacket(this);
|
||||
case FitnessData.MessageCount.sleepId:
|
||||
return new FitnessData.MessageCount.Response(paramsProvider).fromPacket(this);
|
||||
case FitnessData.MessageData.sleepId:
|
||||
return new FitnessData.MessageData.SleepResponse(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case Alarms.id:
|
||||
switch (this.commandId) {
|
||||
case Alarms.EventAlarmsList.id:
|
||||
return new Alarms.EventAlarmsList.Response(paramsProvider).fromPacket(this);
|
||||
case Alarms.SmartAlarmList.id:
|
||||
return new Alarms.SmartAlarmList.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case FindPhone.id:
|
||||
if (this.commandId == FindPhone.Response.id)
|
||||
return new FindPhone.Response(paramsProvider).fromPacket(this);
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
case Workout.id:
|
||||
switch (this.commandId) {
|
||||
case Workout.WorkoutCount.id:
|
||||
return new Workout.WorkoutCount.Response(paramsProvider).fromPacket(this);
|
||||
case Workout.WorkoutTotals.id:
|
||||
return new Workout.WorkoutTotals.Response(paramsProvider).fromPacket(this);
|
||||
case Workout.WorkoutData.id:
|
||||
return new Workout.WorkoutData.Response(paramsProvider).fromPacket(this);
|
||||
case Workout.WorkoutPace.id:
|
||||
return new Workout.WorkoutPace.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case MusicControl.id:
|
||||
switch (this.commandId) {
|
||||
case MusicControl.MusicStatusResponse.id:
|
||||
return new MusicControl.MusicStatusResponse(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicInfo.id:
|
||||
return new MusicControl.MusicInfo.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.Control.id:
|
||||
return new MusicControl.Control.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case AccountRelated.id:
|
||||
switch(this.commandId) {
|
||||
case AccountRelated.SendAccountToDevice.id:
|
||||
return new AccountRelated.SendAccountToDevice.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public HuaweiPacket parseOutgoing(byte[] data) throws ParseException {
|
||||
parseData(data);
|
||||
if (!this.complete)
|
||||
return this;
|
||||
|
||||
// TODO: complete
|
||||
|
||||
switch (this.serviceId) {
|
||||
case DeviceConfig.id:
|
||||
switch (this.commandId) {
|
||||
case DeviceConfig.SupportedServices.id:
|
||||
return new DeviceConfig.SupportedServices.OutgoingRequest(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.DateFormat.id:
|
||||
return new DeviceConfig.DateFormat.OutgoingRequest(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.Bond.id:
|
||||
return new DeviceConfig.Bond.OutgoingRequest(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.HiChain.id:
|
||||
return new DeviceConfig.HiChain.OutgoingRequest(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> serializeSliced(byte[] serializedTLV) {
|
||||
List<byte[]> retv = new ArrayList<>();
|
||||
int headerLength = 5; // Magic + (short)(bodyLength + 1) + 0x00 + extra slice info
|
||||
int bodyHeaderLength = 2; // sID + cID
|
||||
int footerLength = 2; //CRC16
|
||||
int maxBodySize = paramsProvider.getSliceSize() - headerLength - footerLength;
|
||||
int packetCount = (int) Math.ceil(((double) serializedTLV.length + (double) bodyHeaderLength) / (double) maxBodySize);
|
||||
|
||||
if (packetCount == 1)
|
||||
return serializeUnsliced(serializedTLV);
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(serializedTLV);
|
||||
byte slice = 0x01;
|
||||
byte flag = 0x00;
|
||||
for (int i = 0; i < packetCount; i++) {
|
||||
short packetSize = (short) Math.min(paramsProvider.getSliceSize(), buffer.remaining() + headerLength + footerLength);
|
||||
|
||||
ByteBuffer packet = ByteBuffer.allocate(packetSize);
|
||||
|
||||
short contentSize = (short) (packetSize - headerLength - footerLength);
|
||||
int start = packet.position();
|
||||
|
||||
packet.put((byte) 0x5a); // Magic byte
|
||||
packet.putShort((short) (packetSize - headerLength)); // Length
|
||||
|
||||
if (i == packetCount - 1)
|
||||
slice = 0x03;
|
||||
|
||||
packet.put(slice); // Slice
|
||||
packet.put(flag); // Flag
|
||||
flag += 1;
|
||||
|
||||
if (slice == 0x01) {
|
||||
packet.put(this.serviceId); // Service ID
|
||||
packet.put(this.commandId); // Command ID
|
||||
slice = 0x02;
|
||||
contentSize -= 2; // To prevent taking too much data
|
||||
}
|
||||
|
||||
byte[] packetContent = new byte[contentSize];
|
||||
buffer.get(packetContent);
|
||||
packet.put(packetContent); // Packet data
|
||||
|
||||
int length = packet.position() - start;
|
||||
if (length != packetSize - footerLength) {
|
||||
// TODO: exception?
|
||||
LOG.error(String.format(GBApplication.getLanguage(), "Packet lengths don't match! %d != %d", length, packetSize + headerLength));
|
||||
}
|
||||
|
||||
byte[] complete = new byte[length];
|
||||
packet.position(start);
|
||||
packet.get(complete, 0, length);
|
||||
int crc16 = CheckSums.getCRC16(complete, 0x0000);
|
||||
|
||||
packet.putShort((short) crc16); // CRC16
|
||||
|
||||
retv.add(packet.array());
|
||||
}
|
||||
return retv;
|
||||
}
|
||||
|
||||
private List<byte[]> serializeUnsliced(byte[] serializedTLV) {
|
||||
List<byte[]> retv = new ArrayList<>();
|
||||
int headerLength = 4; // Magic + (short)(bodyLength + 1) + 0x00
|
||||
int bodyHeaderLength = 2; // sID + cID
|
||||
int footerLength = 2; //CRC16
|
||||
int bodyLength = bodyHeaderLength + serializedTLV.length;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(headerLength + bodyLength);
|
||||
buffer.put((byte) 0x5A);
|
||||
buffer.putShort((short)(bodyLength + 1));
|
||||
buffer.put((byte) 0x00);
|
||||
buffer.put(this.serviceId);
|
||||
buffer.put(this.commandId);
|
||||
buffer.put(serializedTLV);
|
||||
int crc16 = CheckSums.getCRC16(buffer.array(), 0x0000);
|
||||
ByteBuffer finalBuffer = ByteBuffer.allocate(buffer.capacity() + footerLength);
|
||||
finalBuffer.put(buffer.array());
|
||||
finalBuffer.putShort((short)crc16);
|
||||
retv.add(finalBuffer.array());
|
||||
return retv;
|
||||
}
|
||||
|
||||
public List<byte[]> serialize() throws CryptoException {
|
||||
// TODO: necessary for this to work:
|
||||
// - serviceId
|
||||
// - commandId
|
||||
// - tlv
|
||||
// TODO: maybe use the complete flag to know if it can be serialized?
|
||||
|
||||
HuaweiTLV serializableTlv;
|
||||
if (this.isEncrypted && this.paramsProvider.areTransactionsCrypted()) {
|
||||
try {
|
||||
serializableTlv = this.tlv.encrypt(paramsProvider);
|
||||
} catch (HuaweiCrypto.CryptoException e) {
|
||||
throw new CryptoException("Encrypt exception", e);
|
||||
}
|
||||
} else {
|
||||
serializableTlv = this.tlv;
|
||||
}
|
||||
|
||||
byte[] serializedTLV = serializableTlv.serialize();
|
||||
List<byte[]> retv;
|
||||
if (isSliced) {
|
||||
retv = serializeSliced(serializedTLV);
|
||||
} else {
|
||||
retv = serializeUnsliced(serializedTLV);
|
||||
}
|
||||
return retv;
|
||||
}
|
||||
|
||||
public HuaweiTLV getTlv() {
|
||||
return this.tlv;
|
||||
}
|
||||
|
||||
public void setTlv(HuaweiTLV tlv) {
|
||||
this.tlv = tlv;
|
||||
}
|
||||
|
||||
public void setEncryption(boolean b) {
|
||||
this.isEncrypted = b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
HuaweiPacket that = (HuaweiPacket) o;
|
||||
|
||||
if (serviceId != that.serviceId) return false;
|
||||
if (commandId != that.commandId) return false;
|
||||
if (complete != that.complete) return false;
|
||||
if (isEncrypted != that.isEncrypted) return false;
|
||||
return Objects.equals(tlv, that.tlv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HuaweiPacket{" +
|
||||
"paramsProvider=" + paramsProvider +
|
||||
", serviceId=" + serviceId +
|
||||
", commandId=" + commandId +
|
||||
", tlv=" + tlv +
|
||||
", partialPacket=" + Arrays.toString(partialPacket) +
|
||||
", payload=" + Arrays.toString(payload) +
|
||||
", complete=" + complete +
|
||||
", isEncrypted=" + isEncrypted +
|
||||
", isSliced=" + isSliced +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,530 @@
|
||||
/* Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
|
||||
public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivitySample> {
|
||||
|
||||
/*
|
||||
* We save all data by saving a marker at the begin and end.
|
||||
* Meaning of fields that are not self-explanatory:
|
||||
* - `otherTimestamp`
|
||||
* The timestamp of the other marker, if it's larger this is the begin, otherwise the end
|
||||
* - `source`
|
||||
* The source of the data, which Huawei Band message the data came from
|
||||
*/
|
||||
|
||||
private static class RawTypes {
|
||||
public static final int NOT_MEASURED = -1;
|
||||
|
||||
public static final int UNKNOWN = 1;
|
||||
|
||||
public static final int DEEP_SLEEP = 0x07;
|
||||
public static final int LIGHT_SLEEP = 0x06;
|
||||
}
|
||||
|
||||
public HuaweiSampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(int rawType) {
|
||||
switch (rawType) {
|
||||
case RawTypes.DEEP_SLEEP:
|
||||
return ActivityKind.TYPE_DEEP_SLEEP;
|
||||
case RawTypes.LIGHT_SLEEP:
|
||||
return ActivityKind.TYPE_LIGHT_SLEEP;
|
||||
default:
|
||||
return ActivityKind.TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(int activityKind) {
|
||||
switch (activityKind) {
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
return RawTypes.DEEP_SLEEP;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
return RawTypes.LIGHT_SLEEP;
|
||||
default:
|
||||
return RawTypes.NOT_MEASURED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
return rawIntensity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<HuaweiActivitySample, ?> getSampleDao() {
|
||||
return getSession().getHuaweiActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return HuaweiActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return HuaweiActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return HuaweiActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HuaweiActivitySample createActivitySample() {
|
||||
return new HuaweiActivitySample();
|
||||
}
|
||||
|
||||
private int getLastFetchTimestamp(QueryBuilder<HuaweiActivitySample> qb) {
|
||||
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null)
|
||||
return 0;
|
||||
Property deviceProperty = HuaweiActivitySampleDao.Properties.DeviceId;
|
||||
Property timestampProperty = HuaweiActivitySampleDao.Properties.Timestamp;
|
||||
|
||||
qb.where(deviceProperty.eq(dbDevice.getId()))
|
||||
.orderDesc(timestampProperty)
|
||||
.limit(1);
|
||||
|
||||
List<HuaweiActivitySample> samples = qb.build().list();
|
||||
if (samples.isEmpty())
|
||||
return 0;
|
||||
|
||||
HuaweiActivitySample sample = samples.get(0);
|
||||
return sample.getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last timestamp where the sleep data has been fully synchronized
|
||||
* @return Last fully synchronized timestamp for sleep data
|
||||
*/
|
||||
public int getLastSleepFetchTimestamp() {
|
||||
QueryBuilder<HuaweiActivitySample> qb = getSampleDao().queryBuilder();
|
||||
Property sourceProperty = HuaweiActivitySampleDao.Properties.Source;
|
||||
Property activityTypeProperty = HuaweiActivitySampleDao.Properties.RawKind;
|
||||
|
||||
qb.where(sourceProperty.eq(0x0d), activityTypeProperty.eq(0x01));
|
||||
|
||||
return getLastFetchTimestamp(qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last timestamp where the step data has been fully synchronized
|
||||
* @return Last fully synchronized timestamp for step data
|
||||
*/
|
||||
public int getLastStepFetchTimestamp() {
|
||||
QueryBuilder<HuaweiActivitySample> qb = getSampleDao().queryBuilder();
|
||||
Property sourceProperty = HuaweiActivitySampleDao.Properties.Source;
|
||||
|
||||
qb.where(sourceProperty.eq(0x0b));
|
||||
|
||||
return getLastFetchTimestamp(qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of a sample
|
||||
* @param sample The sample to copy
|
||||
* @return The copy of the sample
|
||||
*/
|
||||
private HuaweiActivitySample copySample(HuaweiActivitySample sample) {
|
||||
HuaweiActivitySample sampleCopy = new HuaweiActivitySample(
|
||||
sample.getTimestamp(),
|
||||
sample.getDeviceId(),
|
||||
sample.getUserId(),
|
||||
sample.getOtherTimestamp(),
|
||||
sample.getSource(),
|
||||
sample.getRawKind(),
|
||||
sample.getRawIntensity(),
|
||||
sample.getSteps(),
|
||||
sample.getCalories(),
|
||||
sample.getDistance(),
|
||||
sample.getSpo(),
|
||||
sample.getHeartRate()
|
||||
);
|
||||
sampleCopy.setProvider(sample.getProvider());
|
||||
return sampleCopy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGBActivitySample(HuaweiActivitySample activitySample) {
|
||||
HuaweiActivitySample start = copySample(activitySample);
|
||||
HuaweiActivitySample end = copySample(activitySample);
|
||||
end.setTimestamp(start.getOtherTimestamp());
|
||||
end.setSteps(ActivitySample.NOT_MEASURED);
|
||||
end.setCalories(ActivitySample.NOT_MEASURED);
|
||||
end.setDistance(ActivitySample.NOT_MEASURED);
|
||||
end.setSpo(ActivitySample.NOT_MEASURED);
|
||||
end.setHeartRate(ActivitySample.NOT_MEASURED);
|
||||
end.setOtherTimestamp(start.getTimestamp());
|
||||
|
||||
getSampleDao().insertOrReplace(start);
|
||||
getSampleDao().insertOrReplace(end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGBActivitySamples(HuaweiActivitySample[] activitySamples) {
|
||||
List<HuaweiActivitySample> newSamples = new ArrayList<>();
|
||||
for (HuaweiActivitySample sample : activitySamples) {
|
||||
HuaweiActivitySample start = copySample(sample);
|
||||
HuaweiActivitySample end = copySample(sample);
|
||||
end.setTimestamp(start.getOtherTimestamp());
|
||||
end.setSteps(ActivitySample.NOT_MEASURED);
|
||||
end.setCalories(ActivitySample.NOT_MEASURED);
|
||||
end.setDistance(ActivitySample.NOT_MEASURED);
|
||||
end.setSpo(ActivitySample.NOT_MEASURED);
|
||||
end.setHeartRate(ActivitySample.NOT_MEASURED);
|
||||
end.setOtherTimestamp(start.getTimestamp());
|
||||
|
||||
newSamples.add(start);
|
||||
newSamples.add(end);
|
||||
}
|
||||
getSampleDao().insertOrReplaceInTx(newSamples);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the activity samples, ordered by timestamp
|
||||
* @param timestampFrom Start timestamp
|
||||
* @param timestampTo End timestamp
|
||||
* @return List of activities between the timestamps, ordered by timestamp
|
||||
*/
|
||||
private List<HuaweiActivitySample> getRawOrderedActivitySamples(int timestampFrom, int timestampTo) {
|
||||
QueryBuilder<HuaweiActivitySample> qb = getSampleDao().queryBuilder();
|
||||
Property timestampProperty = getTimestampSampleProperty();
|
||||
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null) {
|
||||
// no device, no samples
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Property deviceProperty = getDeviceIdentifierSampleProperty();
|
||||
qb.where(deviceProperty.eq(dbDevice.getId()), timestampProperty.ge(timestampFrom))
|
||||
.where(timestampProperty.le(timestampTo))
|
||||
.orderAsc(timestampProperty);
|
||||
List<HuaweiActivitySample> samples = qb.build().list();
|
||||
for (HuaweiActivitySample sample : samples) {
|
||||
sample.setProvider(this);
|
||||
}
|
||||
detachFromSession();
|
||||
return samples;
|
||||
}
|
||||
|
||||
private List<HuaweiWorkoutDataSample> getRawOrderedWorkoutSamplesWithHeartRate(int timestampFrom, int timestampTo) {
|
||||
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null)
|
||||
return Collections.emptyList();
|
||||
|
||||
QueryBuilder<HuaweiWorkoutDataSample> qb = getSession().getHuaweiWorkoutDataSampleDao().queryBuilder();
|
||||
Property timestampProperty = HuaweiWorkoutDataSampleDao.Properties.Timestamp;
|
||||
Property heartRateProperty = HuaweiWorkoutDataSampleDao.Properties.HeartRate;
|
||||
Property deviceProperty = HuaweiWorkoutSummarySampleDao.Properties.DeviceId;
|
||||
qb.join(HuaweiWorkoutDataSampleDao.Properties.WorkoutId, HuaweiWorkoutSummarySample.class, HuaweiWorkoutSummarySampleDao.Properties.WorkoutId)
|
||||
.where(deviceProperty.eq(dbDevice.getId()));
|
||||
qb.where(
|
||||
timestampProperty.ge(timestampFrom),
|
||||
timestampProperty.le(timestampTo),
|
||||
heartRateProperty.notEq(ActivitySample.NOT_MEASURED)
|
||||
).orderAsc(timestampProperty);
|
||||
List<HuaweiWorkoutDataSample> samples = qb.build().list();
|
||||
getSession().getHuaweiWorkoutSummarySampleDao().detachAll();
|
||||
return samples;
|
||||
}
|
||||
|
||||
private static class SampleLoopState {
|
||||
public long deviceId = 0;
|
||||
public long userId = 0;
|
||||
|
||||
int[] activityTypes = {};
|
||||
|
||||
public int sleepModifier = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Note that this does a lot more than the normal implementation, as it takes care of everything
|
||||
* that is necessary for proper displaying of data.
|
||||
*
|
||||
* This essentially boils down to four things:
|
||||
* - It adds in the workout heart rate data
|
||||
* - It adds a sample with intensity zero before start markers (start of block)
|
||||
* - It adds a sample with intensity zero after end markers (end of block)
|
||||
* - It modifies some blocks so the sleep data gets handled correctly
|
||||
* The second and fourth are necessary for proper stats calculation, the third is mostly for
|
||||
* nicer graphs.
|
||||
*
|
||||
* Note that the data in the database isn't changed, as the samples are detached.
|
||||
*/
|
||||
@Override
|
||||
protected List<HuaweiActivitySample> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) {
|
||||
// Note that the result of this function has to be sorted by timestamp!
|
||||
|
||||
List<HuaweiActivitySample> rawSamples = getRawOrderedActivitySamples(timestamp_from, timestamp_to);
|
||||
List<HuaweiWorkoutDataSample> workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to);
|
||||
|
||||
List<HuaweiActivitySample> processedSamples = new ArrayList<>();
|
||||
|
||||
Iterator<HuaweiActivitySample> itRawSamples = rawSamples.iterator();
|
||||
Iterator<HuaweiWorkoutDataSample> itWorkoutSamples = workoutSamples.iterator();
|
||||
|
||||
HuaweiActivitySample nextRawSample = null;
|
||||
if (itRawSamples.hasNext())
|
||||
nextRawSample = itRawSamples.next();
|
||||
HuaweiWorkoutDataSample nextWorkoutSample = null;
|
||||
if (itWorkoutSamples.hasNext())
|
||||
nextWorkoutSample = itWorkoutSamples.next();
|
||||
|
||||
SampleLoopState state = new SampleLoopState();
|
||||
if (nextRawSample != null) {
|
||||
state.deviceId = nextRawSample.getDeviceId();
|
||||
state.userId = nextRawSample.getUserId();
|
||||
}
|
||||
state.activityTypes = ActivityKind.mapToDBActivityTypes(activityType, this);
|
||||
|
||||
while (nextRawSample != null || nextWorkoutSample != null) {
|
||||
if (nextRawSample == null) {
|
||||
processWorkoutSample(processedSamples, state, nextWorkoutSample);
|
||||
|
||||
nextWorkoutSample = null;
|
||||
if (itWorkoutSamples.hasNext())
|
||||
nextWorkoutSample = itWorkoutSamples.next();
|
||||
} else if (nextWorkoutSample == null) {
|
||||
processRawSample(processedSamples, state, nextRawSample);
|
||||
|
||||
nextRawSample = null;
|
||||
if (itRawSamples.hasNext())
|
||||
nextRawSample = itRawSamples.next();
|
||||
} else if (nextRawSample.getTimestamp() > nextWorkoutSample.getTimestamp()) {
|
||||
processWorkoutSample(processedSamples, state, nextWorkoutSample);
|
||||
|
||||
nextWorkoutSample = null;
|
||||
if (itWorkoutSamples.hasNext())
|
||||
nextWorkoutSample = itWorkoutSamples.next();
|
||||
} else {
|
||||
processRawSample(processedSamples, state, nextRawSample);
|
||||
|
||||
nextRawSample = null;
|
||||
if (itRawSamples.hasNext())
|
||||
nextRawSample = itRawSamples.next();
|
||||
}
|
||||
}
|
||||
|
||||
processedSamples = interpolate(processedSamples);
|
||||
|
||||
return processedSamples;
|
||||
}
|
||||
|
||||
private List<HuaweiActivitySample> interpolate(List<HuaweiActivitySample> processedSamples) {
|
||||
List<HuaweiActivitySample> retv = new ArrayList<>();
|
||||
|
||||
if (processedSamples.size() == 0)
|
||||
return retv;
|
||||
|
||||
HuaweiActivitySample lastSample = processedSamples.get(0);
|
||||
retv.add(lastSample);
|
||||
for (int i = 1; i < processedSamples.size() - 1; i++) {
|
||||
HuaweiActivitySample sample = processedSamples.get(i);
|
||||
|
||||
int timediff = sample.getTimestamp() - lastSample.getTimestamp();
|
||||
if (timediff > 60) {
|
||||
if (lastSample.getRawKind() != -1 && sample.getRawKind() != lastSample.getRawKind()) {
|
||||
HuaweiActivitySample postSample = new HuaweiActivitySample(
|
||||
lastSample.getTimestamp() + 1,
|
||||
lastSample.getDeviceId(),
|
||||
lastSample.getUserId(),
|
||||
0,
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
0,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED
|
||||
);
|
||||
postSample.setProvider(this);
|
||||
retv.add(postSample);
|
||||
}
|
||||
|
||||
if (sample.getRawKind() != -1 && sample.getRawKind() != lastSample.getRawKind()) {
|
||||
HuaweiActivitySample preSample = new HuaweiActivitySample(
|
||||
sample.getTimestamp() - 1,
|
||||
sample.getDeviceId(),
|
||||
sample.getUserId(),
|
||||
0,
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
0,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED
|
||||
);
|
||||
preSample.setProvider(this);
|
||||
retv.add(preSample);
|
||||
}
|
||||
}
|
||||
|
||||
retv.add(sample);
|
||||
lastSample = sample;
|
||||
}
|
||||
|
||||
if (lastSample.getRawKind() != -1) {
|
||||
HuaweiActivitySample postSample = new HuaweiActivitySample(
|
||||
lastSample.getTimestamp() + 1,
|
||||
lastSample.getDeviceId(),
|
||||
lastSample.getUserId(),
|
||||
0,
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
0,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED
|
||||
);
|
||||
postSample.setProvider(this);
|
||||
retv.add(postSample);
|
||||
}
|
||||
|
||||
return retv;
|
||||
}
|
||||
|
||||
private void processRawSample(List<HuaweiActivitySample> processedSamples, SampleLoopState state, HuaweiActivitySample sample) {
|
||||
// Filter on Source 0x0d, Type 0x01, until we know what it is and how we should handle them.
|
||||
// Just showing them currently has some issues.
|
||||
if (sample.getSource() == FitnessData.MessageData.sleepId && sample.getRawKind() == RawTypes.UNKNOWN)
|
||||
return;
|
||||
|
||||
HuaweiActivitySample lastSample = null;
|
||||
|
||||
boolean isStartMarker = sample.getTimestamp() < sample.getOtherTimestamp();
|
||||
|
||||
// Handle preferences for wakeup status ignore - can fix some quirks on some devices
|
||||
if (sample.getRawKind() == 0x08) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
if (isStartMarker && prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_IGNORE_WAKEUP_STATUS_START, false))
|
||||
return;
|
||||
if (!isStartMarker && prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_IGNORE_WAKEUP_STATUS_END, false))
|
||||
return;
|
||||
}
|
||||
|
||||
// Backdate the end marker by one - otherwise the interpolation fails
|
||||
if (sample.getTimestamp() > sample.getOtherTimestamp())
|
||||
sample.setTimestamp(sample.getTimestamp() - 1);
|
||||
|
||||
if (processedSamples.size() > 0)
|
||||
lastSample = processedSamples.get(processedSamples.size() - 1);
|
||||
if (lastSample != null && lastSample.getTimestamp() == sample.getTimestamp()) {
|
||||
// Merge the samples - only if there isn't any data yet, except the kind
|
||||
|
||||
if (lastSample.getRawKind() == -1)
|
||||
lastSample.setRawKind(sample.getRawKind());
|
||||
// Do overwrite the kind if the new sample is a starting sample
|
||||
if (isStartMarker && sample.getRawKind() != -1) {
|
||||
lastSample.setRawKind(sample.getRawKind());
|
||||
lastSample.setOtherTimestamp(sample.getOtherTimestamp()); // Necessary for interpolation
|
||||
}
|
||||
|
||||
if (lastSample.getRawIntensity() == -1)
|
||||
lastSample.setRawIntensity(sample.getRawIntensity());
|
||||
if (lastSample.getSteps() == -1)
|
||||
lastSample.setSteps(sample.getSteps());
|
||||
if (lastSample.getCalories() == -1)
|
||||
lastSample.setCalories(sample.getCalories());
|
||||
if (lastSample.getDistance() == -1)
|
||||
lastSample.setDistance(sample.getDistance());
|
||||
if (lastSample.getSpo() == -1)
|
||||
lastSample.setSpo(sample.getSpo());
|
||||
if (lastSample.getHeartRate() == -1)
|
||||
lastSample.setHeartRate(sample.getHeartRate());
|
||||
if (lastSample.getSource() != sample.getSource())
|
||||
lastSample.setSource((byte) 0x00);
|
||||
} else {
|
||||
if (state.sleepModifier != 0)
|
||||
sample.setRawKind(state.sleepModifier);
|
||||
processedSamples.add(sample);
|
||||
}
|
||||
|
||||
if (sample.getSource() == FitnessData.MessageData.sleepId && (sample.getRawKind() == RawTypes.LIGHT_SLEEP || sample.getRawKind() == RawTypes.DEEP_SLEEP)) {
|
||||
if (isStartMarker)
|
||||
state.sleepModifier = sample.getRawKind();
|
||||
else
|
||||
state.sleepModifier = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void processWorkoutSample(List<HuaweiActivitySample> processedSamples, SampleLoopState state, HuaweiWorkoutDataSample workoutSample) {
|
||||
processRawSample(processedSamples, state, convertWorkoutSampleToActivitySample(workoutSample, state));
|
||||
}
|
||||
|
||||
private HuaweiActivitySample convertWorkoutSampleToActivitySample(HuaweiWorkoutDataSample workoutSample, SampleLoopState state) {
|
||||
int hr = workoutSample.getHeartRate() & 0xFF;
|
||||
HuaweiActivitySample newSample = new HuaweiActivitySample(
|
||||
workoutSample.getTimestamp(),
|
||||
state.deviceId,
|
||||
state.userId,
|
||||
0,
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
hr
|
||||
);
|
||||
newSample.setProvider(this);
|
||||
return newSample;
|
||||
}
|
||||
}
|
@ -0,0 +1,254 @@
|
||||
/* Copyright (C) 2021 José Rebelo
|
||||
Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Parcel;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.Collections;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_DEBUG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_DEBUG_REQUEST;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_TRUSLEEP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_WORKMODE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO;
|
||||
|
||||
public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
final GBDevice device;
|
||||
final HuaweiCoordinator coordinator;
|
||||
|
||||
public HuaweiSettingsCustomizer(final GBDevice device) {
|
||||
this.device = device;
|
||||
this.coordinator = ((HuaweiCoordinatorSupplier) this.device.getDeviceCoordinator()).getHuaweiCoordinator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
|
||||
if (preference.getKey().equals(PREF_DO_NOT_DISTURB)) {
|
||||
final String dndState = ((ListPreference) preference).getValue();
|
||||
final XTimePreference dndStart = handler.findPreference(PREF_DO_NOT_DISTURB_START);
|
||||
final XTimePreference dndEnd = handler.findPreference(PREF_DO_NOT_DISTURB_END);
|
||||
final SwitchPreferenceCompat dndLifWrist = handler.findPreference(PREF_DO_NOT_DISTURB_LIFT_WRIST);
|
||||
final SwitchPreferenceCompat dndNotWear = handler.findPreference(PREF_DO_NOT_DISTURB_NOT_WEAR);
|
||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
boolean statusLiftWrist = sharedPrefs.getBoolean(PREF_LIFTWRIST_NOSHED, false);
|
||||
|
||||
dndStart.setEnabled(false);
|
||||
dndEnd.setEnabled(false);
|
||||
dndNotWear.setEnabled(false);
|
||||
dndLifWrist.setEnabled(false);
|
||||
if (dndState.equals("scheduled")) {
|
||||
dndStart.setEnabled(true);
|
||||
dndEnd.setEnabled(true);
|
||||
}
|
||||
if (statusLiftWrist && !dndState.equals("off")) {
|
||||
dndLifWrist.setEnabled(true);
|
||||
}
|
||||
if (dndState.equals("off")) {
|
||||
dndNotWear.setEnabled(true);
|
||||
}
|
||||
}
|
||||
if (preference.getKey().equals("huawei_reparse_workout_data")) {
|
||||
if (((SwitchPreferenceCompat) preference).isChecked()) {
|
||||
GB.toast("Starting workout reparse", Toast.LENGTH_SHORT, 0);
|
||||
HuaweiWorkoutGbParser.parseAllWorkouts();
|
||||
GB.toast("Workout reparse is complete", Toast.LENGTH_SHORT, 0);
|
||||
|
||||
((SwitchPreferenceCompat) preference).setChecked(false);
|
||||
}
|
||||
}
|
||||
if (preference.getKey().equals(PREF_FORCE_OPTIONS)) {
|
||||
final Preference dnd = handler.findPreference("screen_do_not_disturb");
|
||||
if (dnd != null) {
|
||||
dnd.setVisible(false);
|
||||
if (this.coordinator.supportsDoNotDisturb(handler.getDevice()))
|
||||
dnd.setVisible(true);
|
||||
}
|
||||
final ListPreference wearLocation = handler.findPreference(PREF_WEARLOCATION);
|
||||
wearLocation.setVisible(false);
|
||||
if (this.coordinator.supportsWearLocation(handler.getDevice())) {
|
||||
wearLocation.setVisible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, Prefs prefs) {
|
||||
|
||||
handler.addPreferenceHandlerFor(PREF_FORCE_OPTIONS);
|
||||
handler.addPreferenceHandlerFor(PREF_FORCE_ENABLE_SMART_ALARM);
|
||||
handler.addPreferenceHandlerFor(PREF_FORCE_ENABLE_WEAR_LOCATION);
|
||||
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_WORKMODE);
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_TRUSLEEP);
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_DEBUG);
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_DEBUG_REQUEST);
|
||||
|
||||
// Only supported on specific devices
|
||||
final ListPreference languageSetting = handler.findPreference(PREF_LANGUAGE);
|
||||
if (languageSetting != null) {
|
||||
languageSetting.setVisible(false);
|
||||
if (this.coordinator.supportsLanguageSetting())
|
||||
languageSetting.setVisible(true);
|
||||
}
|
||||
|
||||
final Preference dnd = handler.findPreference("screen_do_not_disturb");
|
||||
if (dnd != null) {
|
||||
dnd.setVisible(false);
|
||||
if (this.coordinator.supportsDoNotDisturb(handler.getDevice()))
|
||||
dnd.setVisible(true);
|
||||
}
|
||||
|
||||
final Preference trusleep = handler.findPreference(PREF_HUAWEI_TRUSLEEP);
|
||||
if (trusleep != null) {
|
||||
trusleep.setVisible(false);
|
||||
if (this.coordinator.supportsTruSleep())
|
||||
trusleep.setVisible(true);
|
||||
}
|
||||
|
||||
// if (this.coordinator.supportsHeartRate())
|
||||
// dynamicSupportedDeviceSpecificSettings.add(R.xml.devicesettings_heartrate_automatic_enable);
|
||||
|
||||
final Preference inactivity = handler.findPreference("screen_inactivity");
|
||||
if (inactivity != null) {
|
||||
inactivity.setVisible(false);
|
||||
if (this.coordinator.supportsInactivityWarnings())
|
||||
inactivity.setVisible(true);
|
||||
}
|
||||
|
||||
final ListPreference wearLocation = handler.findPreference(PREF_WEARLOCATION);
|
||||
if (wearLocation != null) {
|
||||
wearLocation.setVisible(false);
|
||||
if (this.coordinator.supportsWearLocation(handler.getDevice()))
|
||||
wearLocation.setVisible(true);
|
||||
}
|
||||
|
||||
final ListPreference date = handler.findPreference(PREF_DATEFORMAT);
|
||||
final ListPreference time = handler.findPreference(PREF_TIMEFORMAT);
|
||||
if (date != null) {
|
||||
date.setVisible(false);
|
||||
time.setVisible(false);
|
||||
if (this.coordinator.supportsDateFormat()) {
|
||||
date.setVisible(true);
|
||||
time.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
final ListPreference workmode = handler.findPreference(PREF_HUAWEI_WORKMODE);
|
||||
if (workmode != null) {
|
||||
workmode.setVisible(false);
|
||||
if (this.coordinator.supportsAutoWorkMode())
|
||||
workmode.setVisible(true);
|
||||
}
|
||||
|
||||
final SwitchPreferenceCompat liftwirst = handler.findPreference(PREF_LIFTWRIST_NOSHED);
|
||||
if (liftwirst != null) {
|
||||
liftwirst.setVisible(false);
|
||||
if (this.coordinator.supportsActivateOnLift())
|
||||
liftwirst.setVisible(true);
|
||||
}
|
||||
|
||||
final SwitchPreferenceCompat rotatewirst = handler.findPreference(PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO);
|
||||
if (rotatewirst != null) {
|
||||
rotatewirst.setVisible(false);
|
||||
if (this.coordinator.supportsRotateToCycleInfo())
|
||||
rotatewirst.setVisible(true);
|
||||
}
|
||||
|
||||
final Preference forceOptions = handler.findPreference(PREF_FORCE_OPTIONS);
|
||||
if (forceOptions != null) {
|
||||
forceOptions.setVisible(false);
|
||||
boolean supportsSmartAlarm = this.coordinator.supportsSmartAlarm();
|
||||
boolean supportsWearLocation = this.coordinator.supportsWearLocation();
|
||||
if (!supportsSmartAlarm || !supportsWearLocation) {
|
||||
forceOptions.setVisible(true);
|
||||
final SwitchPreferenceCompat forceSmartAlarm = handler.findPreference(PREF_FORCE_ENABLE_SMART_ALARM);
|
||||
forceSmartAlarm.setVisible(false);
|
||||
if (!supportsSmartAlarm) {
|
||||
forceSmartAlarm.setVisible(true);
|
||||
}
|
||||
final SwitchPreferenceCompat forceWearLocation = handler.findPreference(PREF_FORCE_ENABLE_WEAR_LOCATION);
|
||||
forceWearLocation.setVisible(false);
|
||||
if (!supportsWearLocation) {
|
||||
forceWearLocation.setVisible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final SwitchPreferenceCompat disconnectNotification = handler.findPreference(PREF_DISCONNECTNOTIF_NOSHED);
|
||||
if (disconnectNotification != null) {
|
||||
disconnectNotification.setVisible(false);
|
||||
if (this.coordinator.supportsNotificationOnBluetoothLoss())
|
||||
disconnectNotification.setVisible(true);
|
||||
}
|
||||
|
||||
final SwitchPreferenceCompat reparseWorkout = handler.findPreference("huawei_reparse_workout_data");
|
||||
if (reparseWorkout != null) {
|
||||
reparseWorkout.setVisible(false);
|
||||
if (this.coordinator.supportsWorkouts())
|
||||
reparseWorkout.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeParcelable(device, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPreferenceKeysWithSummary() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
|
||||
public static final Creator<HuaweiSettingsCustomizer> CREATOR= new Creator<HuaweiSettingsCustomizer>() {
|
||||
|
||||
@Override
|
||||
public HuaweiSettingsCustomizer createFromParcel(Parcel parcel) {
|
||||
final GBDevice device = parcel.readParcelable(HuaweiSettingsCustomizer.class.getClassLoader());
|
||||
return new HuaweiSettingsCustomizer(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HuaweiSettingsCustomizer[] newArray(int i) {
|
||||
return new HuaweiSettingsCustomizer[0];
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
/* Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractSpo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider<HuaweiSpo2SampleProvider.HuaweiSpo2Sample> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiSpo2SampleProvider.class);
|
||||
|
||||
private final HuaweiSampleProvider huaweiSampleProvider;
|
||||
|
||||
public HuaweiSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
this.huaweiSampleProvider = new HuaweiSampleProvider(this.getDevice(), this.getSession());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an Huawei activity sample to an SpO2 sample
|
||||
* @param sample Activity sample to convert
|
||||
* @return SpO sample containing the SpO value, timestamp, userID, and deviceID of the activity sample
|
||||
*/
|
||||
@NonNull
|
||||
private HuaweiSpo2Sample activityToSpo2Sample(HuaweiActivitySample sample) {
|
||||
return new HuaweiSpo2Sample(
|
||||
-1, // No difference between auto and manual for Huawei
|
||||
sample.getTimestamp() * 1000L,
|
||||
sample.getUserId(),
|
||||
sample.getDeviceId(),
|
||||
sample.getSpo()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<HuaweiSpo2Sample> getAllSamples(long timestampFrom, long timestampTo) {
|
||||
List<HuaweiActivitySample> activitySamples = huaweiSampleProvider.getAllActivitySamples((int) (timestampFrom / 1000L), (int) (timestampTo / 1000L));
|
||||
List<HuaweiSpo2Sample> spo2Samples = new ArrayList<>(activitySamples.size());
|
||||
for (HuaweiActivitySample sample : activitySamples) {
|
||||
if (sample.getSpo() == -1)
|
||||
continue;
|
||||
spo2Samples.add(activityToSpo2Sample(sample));
|
||||
}
|
||||
return spo2Samples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSample(HuaweiSpo2Sample activitySample) {
|
||||
LOG.error("Huawei Spo2 sample provider addSample called!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSamples(List<HuaweiSpo2Sample> activitySamples) {
|
||||
LOG.error("Huawei Spo2 sample provider addSamples called!");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public HuaweiSpo2Sample getLatestSample() {
|
||||
QueryBuilder<HuaweiActivitySample> qb = this.huaweiSampleProvider.getSampleDao().queryBuilder();
|
||||
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null)
|
||||
return null;
|
||||
final Property deviceProperty = this.huaweiSampleProvider.getDeviceIdentifierSampleProperty();
|
||||
qb
|
||||
.where(deviceProperty.eq(dbDevice.getId()))
|
||||
.where(HuaweiActivitySampleDao.Properties.Spo.notEq(-1))
|
||||
.orderDesc(this.huaweiSampleProvider.getTimestampSampleProperty())
|
||||
.limit(1);
|
||||
final List<HuaweiActivitySample> samples = qb.build().list();
|
||||
if (samples.isEmpty())
|
||||
return null;
|
||||
return activityToSpo2Sample(samples.get(0));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public HuaweiSpo2Sample getFirstSample() {
|
||||
QueryBuilder<HuaweiActivitySample> qb = this.huaweiSampleProvider.getSampleDao().queryBuilder();
|
||||
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null)
|
||||
return null;
|
||||
final Property deviceProperty = this.huaweiSampleProvider.getDeviceIdentifierSampleProperty();
|
||||
qb
|
||||
.where(deviceProperty.eq(dbDevice.getId()))
|
||||
.where(HuaweiActivitySampleDao.Properties.Spo.notEq(-1))
|
||||
.orderAsc(this.huaweiSampleProvider.getTimestampSampleProperty())
|
||||
.limit(1);
|
||||
final List<HuaweiActivitySample> samples = qb.build().list();
|
||||
if (samples.isEmpty())
|
||||
return null;
|
||||
return activityToSpo2Sample(samples.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void detachFromSession() {
|
||||
// Not necessary to do anything here
|
||||
LOG.warn("Huawei Spo2 sample provider detachFromSession called!");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<HuaweiSpo2Sample, ?> getSampleDao() {
|
||||
// This not existing is not an issue (at the time of writing), as this is only used in
|
||||
// methods that are overwritten by this class itself.
|
||||
LOG.error("Huawei Spo2 sample provider getSampleDao called!");
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
LOG.warn("Huawei Spo2 sample provider getTimestampSampleProperty called!");
|
||||
return this.huaweiSampleProvider.getTimestampSampleProperty();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
LOG.warn("Huawei Spo2 sample provider getDeviceIdentifierSampleProperty called!");
|
||||
return this.huaweiSampleProvider.getDeviceIdentifierSampleProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HuaweiSpo2Sample createSample() {
|
||||
return new HuaweiSpo2Sample();
|
||||
}
|
||||
|
||||
public static class HuaweiSpo2Sample extends AbstractSpo2Sample {
|
||||
private int typeNum;
|
||||
private long timestamp;
|
||||
private long userId;
|
||||
private long deviceId;
|
||||
private int spo2;
|
||||
|
||||
public HuaweiSpo2Sample() { }
|
||||
|
||||
public HuaweiSpo2Sample(int typeNum, long timestamp, long userId, long deviceId, int spo2) {
|
||||
this.typeNum = typeNum;
|
||||
this.timestamp = timestamp;
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
this.spo2 = spo2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTypeNum() {
|
||||
return typeNum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTypeNum(int num) {
|
||||
this.typeNum = num;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserId(long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDeviceId(long deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpo2() {
|
||||
return spo2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,400 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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/>. */
|
||||
|
||||
/* TLV parsing and serialisation thanks to https://github.com/yihleego/tlv */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.CryptoTags;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto.CryptoException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.ParamsProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class HuaweiTLV {
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
HuaweiTLV huaweiTLV = (HuaweiTLV) o;
|
||||
return Objects.equals(valueMap, huaweiTLV.valueMap);
|
||||
}
|
||||
|
||||
public static class TLV {
|
||||
private final byte tag;
|
||||
private final byte[] value;
|
||||
|
||||
public TLV(byte tag, byte[] value) {
|
||||
this.tag = tag;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return 1 + VarInt.getVarIntSize(value.length) + value.length;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return ByteBuffer.allocate(this.length())
|
||||
.put(tag)
|
||||
.put(VarInt.putVarIntValue(value.length))
|
||||
.put(value)
|
||||
.array();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{tag: " + Integer.toHexString(tag & 0xFF) + " - Value: " + StringUtils.bytesToHex(value) + "} - ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
TLV tlv = (TLV) o;
|
||||
return tag == tlv.tag && Arrays.equals(value, tlv.value);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiTLV.class);
|
||||
|
||||
protected List<TLV> valueMap;
|
||||
|
||||
public HuaweiTLV() {
|
||||
this.valueMap = new ArrayList<>();
|
||||
}
|
||||
|
||||
public int length() {
|
||||
int length = 0;
|
||||
for (TLV tlv : valueMap)
|
||||
length += tlv.length();
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse byte buffer into this HuaweiTLV
|
||||
* @param buffer The buffer to parse
|
||||
* @param offset The offset to start parsing at
|
||||
* @param length The length to parse
|
||||
* @return The HuaweiTLV object itself
|
||||
* @throws ArrayIndexOutOfBoundsException There are two general cases in which this exception
|
||||
* can be thrown:
|
||||
* 1. offset + length is greater than the buffer length
|
||||
* 2. The buffer is malformed which causes an element size to be larger than the remaining
|
||||
* buffer length
|
||||
*/
|
||||
public HuaweiTLV parse(byte[] buffer, int offset, int length) {
|
||||
if (buffer == null)
|
||||
return null;
|
||||
int parsed = 0;
|
||||
while (parsed < length) {
|
||||
// Tag is 1 byte
|
||||
byte tag = buffer[offset + parsed];
|
||||
parsed += 1;
|
||||
// It seems that there can be an extra null byte at the end of something encrypted
|
||||
// If that happens we ignore it
|
||||
if (parsed == length && tag == 0)
|
||||
break;
|
||||
// Size is a VarInt >= 1 byte
|
||||
VarInt varInt = new VarInt(buffer, offset + parsed);
|
||||
int size = varInt.dValue;
|
||||
parsed += varInt.size;
|
||||
byte[] value = new byte[size];
|
||||
System.arraycopy(buffer, offset + parsed, value, 0, size);
|
||||
put(tag, value);
|
||||
parsed += size;
|
||||
}
|
||||
LOG.debug("Parsed TLV: " + this);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HuaweiTLV parse(byte[] buffer) {
|
||||
if (buffer == null) {
|
||||
return null;
|
||||
}
|
||||
return parse(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
int length = this.length();
|
||||
if (length == 0)
|
||||
return new byte[0];
|
||||
ByteBuffer buffer = ByteBuffer.allocate(length);
|
||||
for (TLV entry : valueMap)
|
||||
buffer.put(entry.serialize());
|
||||
LOG.debug("Serialized TLV: " + this);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag) {
|
||||
byte[] value = new byte[0];
|
||||
valueMap.add(new TLV((byte)tag, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, byte[] value) {
|
||||
if (value == null) {
|
||||
return this;
|
||||
}
|
||||
valueMap.add(new TLV((byte)tag, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, byte value) {
|
||||
return put(tag, new byte[]{value});
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, boolean value) {
|
||||
return put(tag, new byte[]{value ? (byte) 1 : (byte) 0});
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, Long value) {
|
||||
return put(tag, ByteBuffer.allocate(8).putLong(value).array());
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, int value) {
|
||||
return put(tag, ByteBuffer.allocate(4).putInt(value).array());
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, short value) {
|
||||
return put(tag, ByteBuffer.allocate(2).putShort(value).array());
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, String value) {
|
||||
if (value == null) {
|
||||
return this;
|
||||
}
|
||||
return put(tag, value.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, HuaweiTLV value) {
|
||||
if (value == null) {
|
||||
return this;
|
||||
}
|
||||
return put(tag, value.serialize());
|
||||
}
|
||||
|
||||
public List<TLV> get() {
|
||||
return this.valueMap;
|
||||
}
|
||||
|
||||
public byte[] getBytes(int tag) {
|
||||
for (TLV item : valueMap)
|
||||
if (item.getTag() == (byte) tag)
|
||||
return item.getValue();
|
||||
return null;
|
||||
}
|
||||
|
||||
public Byte getByte(int tag) {
|
||||
byte[] bytes = getBytes(tag);
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return bytes[0];
|
||||
}
|
||||
|
||||
public Boolean getBoolean(int tag) {
|
||||
byte[] bytes = getBytes(tag);
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return bytes[0] == 1;
|
||||
}
|
||||
|
||||
public Integer getInteger(int tag) {
|
||||
byte[] bytes = getBytes(tag);
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return ByteBuffer.wrap(bytes).getInt();
|
||||
}
|
||||
|
||||
public Short getShort(int tag) {
|
||||
byte[] bytes = getBytes(tag);
|
||||
if (bytes == null)
|
||||
return null;
|
||||
return ByteBuffer.wrap(bytes).getShort();
|
||||
}
|
||||
|
||||
public String getString(int tag) {
|
||||
byte[] bytes = getBytes(tag);
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public HuaweiTLV getObject(int tag) {
|
||||
byte[] bytes = getBytes(tag);
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return new HuaweiTLV().parse(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public List<HuaweiTLV> getObjects(int tag) {
|
||||
List<HuaweiTLV> returnValue = new ArrayList<>();
|
||||
for (TLV tlv : valueMap) {
|
||||
if (tlv.getTag() == (byte) tag)
|
||||
returnValue.add(new HuaweiTLV().parse(tlv.getValue()));
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public boolean contains(int tag) {
|
||||
for (TLV item : valueMap)
|
||||
if (item.getTag() == (byte) tag)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the last element that was added with the specified tag
|
||||
* @param tag The tag of the element that should be removed
|
||||
* @return The value contained in the removed tag
|
||||
*/
|
||||
public byte[] remove(int tag) {
|
||||
TLV foundItem = null;
|
||||
for (TLV item : valueMap)
|
||||
if (item.getTag() == (byte) tag)
|
||||
foundItem = item;
|
||||
if (foundItem != null) {
|
||||
valueMap.remove(foundItem);
|
||||
return foundItem.getValue();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string representation of HuaweiTLV, "Empty" when no elements are present
|
||||
* @return String
|
||||
*/
|
||||
public String toString() {
|
||||
if (valueMap.isEmpty())
|
||||
return "Empty";
|
||||
|
||||
StringBuilder msg = new StringBuilder();
|
||||
for (TLV entry : valueMap)
|
||||
msg.append(entry.toString());
|
||||
return msg.substring(0, msg.length() - 3);
|
||||
}
|
||||
|
||||
public HuaweiTLV encrypt(ParamsProvider paramsProvider) throws CryptoException {
|
||||
byte[] serializedTLV = serialize();
|
||||
byte[] key = paramsProvider.getSecretKey();
|
||||
byte[] nonce = paramsProvider.getIv();
|
||||
byte[] encryptedTLV = HuaweiCrypto.encrypt(paramsProvider.getAuthMode(), serializedTLV, key, nonce);
|
||||
return new HuaweiTLV()
|
||||
.put(CryptoTags.encryption, (byte) 0x01)
|
||||
.put(CryptoTags.initVector, nonce)
|
||||
.put(CryptoTags.cipherText, encryptedTLV);
|
||||
}
|
||||
|
||||
public void decrypt(ParamsProvider paramsProvider) throws CryptoException {
|
||||
byte[] key = paramsProvider.getSecretKey();
|
||||
byte[] decryptedTLV = HuaweiCrypto.decrypt(paramsProvider.getAuthMode(), getBytes(CryptoTags.cipherText), key, getBytes(CryptoTags.initVector));
|
||||
this.valueMap = new ArrayList<>();
|
||||
parse(decryptedTLV);
|
||||
}
|
||||
}
|
||||
|
||||
final class VarInt {
|
||||
int dValue; // Decoded value of the VarInt
|
||||
int size; // Size of the encoded value
|
||||
byte[] eValue; // Encoded value of the VarInt
|
||||
|
||||
public VarInt(byte[] src, int offset) {
|
||||
this.dValue = getVarIntValue(src, offset);
|
||||
this.eValue = putVarIntValue(this.dValue);
|
||||
this.size = this.eValue.length;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "VarInt(dValue: " + this.dValue + ", size: " + this.size + ", eValue: " + StringUtils.bytesToHex(this.eValue) + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the encoded input value.
|
||||
*
|
||||
* @param value the integer to be measured
|
||||
* @return the encoding size of the input value
|
||||
*/
|
||||
public static int getVarIntSize(int value) {
|
||||
int result = 0;
|
||||
do {
|
||||
result++;
|
||||
value >>>= 7;
|
||||
} while (value != 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a byte array of a variable-length encoding from start,
|
||||
* 7 bits per byte.
|
||||
* Return the decoded value in int.
|
||||
*
|
||||
* @param src the byte array to get the var int from
|
||||
* @return the decoded value in int
|
||||
*/
|
||||
public static int getVarIntValue(byte[] src, int offset) {
|
||||
int result = 0;
|
||||
int b;
|
||||
while (true) {
|
||||
b = src[offset];
|
||||
result += (b & 0x7F);
|
||||
if ((b & 0x80) == 0) { break; }
|
||||
result <<= 7;
|
||||
offset++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an integer in a variable-length encoding, 7 bits per byte.
|
||||
* Return the encoded value in byte[]
|
||||
*
|
||||
* @param value the int value to encode
|
||||
* @return the encoded value in byte[]
|
||||
*/
|
||||
public static byte[] putVarIntValue(int value) {
|
||||
int size = getVarIntSize(value);
|
||||
byte[] result = new byte[size];
|
||||
result[size - 1] = (byte)(value & 0x7F);
|
||||
for (int offset = size - 2; offset >= 0; offset--) {
|
||||
value >>>= 7;
|
||||
result[offset] = (byte)((value & 0x7F) | 0x80);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/* Copyright (C) 2021 Gaignon Damien
|
||||
|
||||
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.huawei;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class HuaweiUtil {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiUtil.class);
|
||||
|
||||
public static byte[] timeToByte(String time) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
DateFormat df = new SimpleDateFormat("HH:mm", Locale.ENGLISH);
|
||||
try {
|
||||
Date t = df.parse(time);
|
||||
assert t != null;
|
||||
calendar.setTime(t);
|
||||
} catch (ParseException e) {
|
||||
LOG.error("Time conversion error: " + e);
|
||||
return null;
|
||||
}
|
||||
return new byte[]{
|
||||
(byte)calendar.get(Calendar.HOUR_OF_DAY),
|
||||
(byte)calendar.get(Calendar.MINUTE)};
|
||||
}
|
||||
|
||||
public static byte[] getTimeAndZoneId() {
|
||||
Calendar now = Calendar.getInstance();
|
||||
int zoneRawOffset = (now.get(Calendar.ZONE_OFFSET) + now.get(Calendar.DST_OFFSET)) / 1000;
|
||||
byte[] id = now.getTimeZone().getID().getBytes();
|
||||
return ByteBuffer.allocate(6 + id.length)
|
||||
.putInt((int)(now.getTimeInMillis() / 1000))
|
||||
.put((byte)(zoneRawOffset < 0 ? (zoneRawOffset / 3600 + 128) : zoneRawOffset / 3600) )
|
||||
.put((byte)(zoneRawOffset / 60 % 60))
|
||||
.put(id)
|
||||
.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.honorband3;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HonorBand3Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HonorBand3Coordinator.class);
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Honor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HONORBAND3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HO_BAND3_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_honor_band3;
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.honorband4;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HonorBand4Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HonorBand4Coordinator.class);
|
||||
|
||||
public HonorBand4Coordinator() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Honor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HONORBAND4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HO_BAND4_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_honor_band4;
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.honorband5;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HonorBand5Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HonorBand5Coordinator.class);
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Honor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HONORBAND5;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HO_BAND5_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_honor_band5;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.honorband6;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HonorBand6Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HonorBand6Coordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HONORBAND6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HO_BAND6_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_honor_band6;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.honorband7;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HonorBand7Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HonorBand7Coordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBAND7;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HO_BAND7_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_honor_band7;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweiband4pro;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HuaweiBand4ProCoordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiBand4ProCoordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBAND4PRO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && (
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_BAND4_NAME) ||
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_BAND4PRO_NAME)
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_band4pro;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweiband6;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HuaweiBand6Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiBand6Coordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBAND6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HU_BAND6_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_band6;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweiband7;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HuaweiBand7Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiBand7Coordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBAND7;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HU_BAND7_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_band7;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei.huaweiband8;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HuaweiBand8Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiBand8Coordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBAND8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HU_BAND8_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_band8;
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweibandaw70;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HuaweiBandAw70Coordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiBandAw70Coordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBANDAW70;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && (
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_BAND3E_NAME) ||
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_BAND4E_NAME)
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_band_aw70;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HuaweiDeviceType getHuaweiType() {
|
||||
return HuaweiDeviceType.AW;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweitalkbandb6;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HuaweiTalkBandB6Coordinator extends HuaweiBRCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiTalkBandB6Coordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEITALKBANDB6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HU_TALKBANDB6_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_talk_band_b6;
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweiwatchgt;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HuaweiWatchGTCoordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatchGTCoordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIWATCHGT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HU_WATCHGT_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_watch_gt;
|
||||
}
|
||||
|
||||
//@Override
|
||||
//public HuaweiDeviceType getHuaweiType() {
|
||||
// Could be SMART
|
||||
// return HuaweiDeviceType.BLE;
|
||||
//}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweiwatchgt2;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HuaweiWatchGT2Coordinator extends HuaweiBRCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatchGT2Coordinator.class);
|
||||
|
||||
public HuaweiWatchGT2Coordinator() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIWATCHGT2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && (
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_WATCHGT2_NAME) ||
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_WATCHGT2PRO_NAME)
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_watchgt2;
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.huaweiwatchgt2e;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
|
||||
public class HuaweiWatchGT2eCoordinator extends HuaweiLECoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatchGT2eCoordinator.class);
|
||||
|
||||
public HuaweiWatchGT2eCoordinator() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIWATCHGT2E;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && name.toLowerCase().startsWith(HuaweiConstants.HU_WATCHGT2E_NAME)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_watchgt2e;
|
||||
}
|
||||
|
||||
//@Override
|
||||
//public HuaweiDeviceType getHuaweiType() {
|
||||
// Could be SMART
|
||||
// return HuaweiDeviceType.BLE;
|
||||
//}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei.huaweiwatchgt3;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HuaweiWatchGT3Coordinator extends HuaweiBRCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatchGT3Coordinator.class);
|
||||
|
||||
public HuaweiWatchGT3Coordinator() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIWATCHGT3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getName();
|
||||
if (name != null && (
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_WATCHGT3_NAME) ||
|
||||
name.toLowerCase().startsWith(HuaweiConstants.HU_WATCHGT3PRO_NAME)
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_watchgt3;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class AccountRelated {
|
||||
public static final byte id = 0x1A;
|
||||
|
||||
public static class SendAccountToDevice {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = AccountRelated.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
// TODO: complete responses
|
||||
|
||||
public class Alarms {
|
||||
|
||||
public static class EventAlarm {
|
||||
public byte index;
|
||||
public boolean status;
|
||||
public byte startHour;
|
||||
public byte startMinute;
|
||||
public byte repeat;
|
||||
public String name;
|
||||
|
||||
public EventAlarm(HuaweiTLV tlv) throws HuaweiPacket.MissingTagException {
|
||||
if (!tlv.contains(0x03))
|
||||
throw new HuaweiPacket.MissingTagException(0x03);
|
||||
if (!tlv.contains(0x04))
|
||||
throw new HuaweiPacket.MissingTagException(0x04);
|
||||
if (!tlv.contains(0x05))
|
||||
throw new HuaweiPacket.MissingTagException(0x05);
|
||||
if (!tlv.contains(0x06))
|
||||
throw new HuaweiPacket.MissingTagException(0x06);
|
||||
if (!tlv.contains(0x07))
|
||||
throw new HuaweiPacket.MissingTagException(0x07);
|
||||
|
||||
this.index = tlv.getByte(0x03);
|
||||
this.status = tlv.getBoolean(0x04);
|
||||
this.startHour = (byte) ((tlv.getShort(0x05) >> 8) & 0xFF);
|
||||
this.startMinute = (byte) (tlv.getShort(0x05) & 0xFF);
|
||||
this.repeat = tlv.getByte(0x06);
|
||||
this.name = tlv.getString(0x07);
|
||||
}
|
||||
|
||||
public EventAlarm(byte index, boolean status, byte startHour, byte startMinute, byte repeat, String name) {
|
||||
this.index = index;
|
||||
this.status = status;
|
||||
this.startHour = startHour;
|
||||
this.startMinute = startMinute;
|
||||
this.repeat = repeat;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public HuaweiTLV asTlv() {
|
||||
return new HuaweiTLV()
|
||||
.put(0x03, index)
|
||||
.put(0x04, status)
|
||||
.put(0x05, (short) ((startHour << 8) | (startMinute & 0xFF)))
|
||||
.put(0x06, repeat)
|
||||
.put(0x07, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EventAlarm{" +
|
||||
"index=" + index +
|
||||
", status=" + status +
|
||||
", startHour=" + startHour +
|
||||
", startMinute=" + startMinute +
|
||||
", repeat=" + repeat +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class SmartAlarm {
|
||||
public byte index;
|
||||
public boolean status;
|
||||
public byte startHour;
|
||||
public byte startMinute;
|
||||
public byte repeat;
|
||||
public byte aheadTime;
|
||||
|
||||
public SmartAlarm(HuaweiTLV tlv) throws HuaweiPacket.MissingTagException {
|
||||
if (!tlv.contains(0x03))
|
||||
throw new HuaweiPacket.MissingTagException(0x03);
|
||||
if (!tlv.contains(0x04))
|
||||
throw new HuaweiPacket.MissingTagException(0x04);
|
||||
if (!tlv.contains(0x05))
|
||||
throw new HuaweiPacket.MissingTagException(0x05);
|
||||
if (!tlv.contains(0x06))
|
||||
throw new HuaweiPacket.MissingTagException(0x06);
|
||||
if (!tlv.contains(0x07))
|
||||
throw new HuaweiPacket.MissingTagException(0x07);
|
||||
|
||||
this.index = tlv.getByte(0x03);
|
||||
this.status = tlv.getBoolean(0x04);
|
||||
this.startHour = (byte) ((tlv.getShort(0x05) >> 8) & 0xFF);
|
||||
this.startMinute = (byte) (tlv.getShort(0x05) & 0xFF);
|
||||
this.repeat = tlv.getByte(0x06);
|
||||
this.aheadTime = tlv.getByte(0x07);
|
||||
}
|
||||
|
||||
public SmartAlarm(boolean status, byte startHour, byte startMinute, byte repeat, byte aheadTime) {
|
||||
this.index = 1;
|
||||
this.status = status;
|
||||
this.startHour = startHour;
|
||||
this.startMinute = startMinute;
|
||||
this.repeat = repeat;
|
||||
this.aheadTime = aheadTime;
|
||||
}
|
||||
|
||||
public HuaweiTLV asTlv() {
|
||||
return new HuaweiTLV()
|
||||
.put(0x03, index)
|
||||
.put(0x04, status)
|
||||
.put(0x05, (short) ((startHour << 8) | (startMinute & 0xFF)))
|
||||
.put(0x06, repeat)
|
||||
.put(0x07, aheadTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SmartAlarm{" +
|
||||
"index=" + index +
|
||||
", status=" + status +
|
||||
", startHour=" + startHour +
|
||||
", startMinute=" + startMinute +
|
||||
", repeat=" + repeat +
|
||||
", aheadTime=" + aheadTime +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static final byte id = 0x08;
|
||||
|
||||
public static class EventAlarmsRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
// TODO: move to list
|
||||
private final HuaweiTLV alarms;
|
||||
|
||||
public EventAlarmsRequest(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Alarms.id;
|
||||
this.commandId = id;
|
||||
|
||||
alarms = new HuaweiTLV();
|
||||
}
|
||||
|
||||
public void addEventAlarm(EventAlarm alarm) {
|
||||
// TODO: 5 is a max and we may need to check for that and throw an exception if passed
|
||||
alarms.put(0x82, alarm.asTlv());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<byte[]> serialize() throws CryptoException {
|
||||
if (this.alarms.get().size() == 0) {
|
||||
// Empty alarms - this will disable them all
|
||||
this.alarms.put(0x82, new HuaweiTLV().put(0x03, (byte) 0x01));
|
||||
}
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x81, this.alarms);
|
||||
this.complete = true;
|
||||
|
||||
return super.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SmartAlarmRequest extends HuaweiPacket {
|
||||
public static final int id = 0x02;
|
||||
|
||||
public SmartAlarmRequest(
|
||||
ParamsProvider paramsProvider,
|
||||
SmartAlarm smartAlarm
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Alarms.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81, new HuaweiTLV()
|
||||
.put(0x82, smartAlarm.asTlv())
|
||||
);
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EventAlarmsList {
|
||||
public static final int id = 0x03;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Alarms.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x01);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public List<EventAlarm> eventAlarms;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x81))
|
||||
throw new MissingTagException(0x81);
|
||||
|
||||
eventAlarms = new ArrayList<>();
|
||||
|
||||
HuaweiTLV tlv = this.tlv.getObject(0x81);
|
||||
for (HuaweiTLV subTlv : tlv.getObjects(0x82)) {
|
||||
eventAlarms.add(new EventAlarm(subTlv));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SmartAlarmList {
|
||||
public static final int id = 0x04;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Alarms.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x01);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public SmartAlarm smartAlarm;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x81))
|
||||
throw new MissingTagException(0x81);
|
||||
|
||||
HuaweiTLV tlv = this.tlv.getObject(0x81);
|
||||
if (tlv.contains(0x82)) {
|
||||
this.smartAlarm = new SmartAlarm(tlv.getObject(0x82));
|
||||
} else {
|
||||
this.smartAlarm = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
|
||||
public class Calls {
|
||||
|
||||
// This doesn't include the initial calling notification, as that is handled
|
||||
// by the Notifications class.
|
||||
|
||||
public static final byte id = 0x04;
|
||||
|
||||
// TODO: tests
|
||||
|
||||
public static class AnswerCallResponse extends HuaweiPacket {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public enum Action {
|
||||
CALL_ACCEPT,
|
||||
CALL_REJECT,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public Action action = Action.UNKNOWN;
|
||||
|
||||
public AnswerCallResponse(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Calls.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.isEncrypted = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws MissingTagException {
|
||||
if (this.tlv.contains(0x01)) {
|
||||
if (this.tlv.getByte(0x01) == 0x01) {
|
||||
this.action = Action.CALL_REJECT;
|
||||
} else if (this.tlv.getByte(0x01) == 0x02) {
|
||||
this.action = Action.CALL_ACCEPT;
|
||||
}
|
||||
// TODO: find more values, if there are any
|
||||
} else {
|
||||
throw new MissingTagException(0x01);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
||||
/* Copyright (C) 2022 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
/*
|
||||
* TODO: It isn't clear if this class should handle more at this point, and thus might need a
|
||||
* different name later
|
||||
*/
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class DisconnectNotification {
|
||||
public static final byte id = 0x0b;
|
||||
|
||||
public static class DisconnectNotificationSetting {
|
||||
public static final byte id = 0x03;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, boolean enable) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = DisconnectNotification.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, enable);
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/* Copyright (C) 2022-2023 Martin.JM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class FindPhone {
|
||||
public static final byte id = 0x0b;
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public boolean start = false;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FindPhone.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.isEncrypted = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() {
|
||||
if (this.tlv.contains(0x01)) {
|
||||
this.start = this.tlv.getBoolean(0x01);
|
||||
}
|
||||
// No missing tag exception so it will stop by default
|
||||
}
|
||||
}
|
||||
|
||||
public static class StopRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x02;
|
||||
|
||||
public StopRequest(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FindPhone.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, (byte) 2);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,563 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class FitnessData {
|
||||
|
||||
public static final byte id = 0x07;
|
||||
|
||||
public static class MotionGoal {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider,
|
||||
byte goalType,
|
||||
byte frameType,
|
||||
int stepGoal,
|
||||
int calorieGoal,
|
||||
short durationGoal) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
frameType = (frameType == 0x01) ? 0x01 : Type.motion;
|
||||
HuaweiTLV subTlv = new HuaweiTLV()
|
||||
.put(0x03, goalType)
|
||||
.put(0x04, frameType);
|
||||
stepGoal = ((Type.data & 0x01) != 0x00) ? stepGoal : 0xffffffff;
|
||||
if (stepGoal != 0xffffffff)
|
||||
subTlv.put(0x05, stepGoal);
|
||||
int calorieGoalFinal = ((Type.data & 0x02) != 0x00) ? calorieGoal : 0xffffffff;
|
||||
if (calorieGoalFinal != 0xffffffff) {
|
||||
subTlv.put(0x06, calorieGoalFinal);
|
||||
} else if (frameType == 0x01) {
|
||||
subTlv.put(0x06, stepGoal / 0x1e);
|
||||
}
|
||||
int distanceGoal = ((Type.data & 0x04) != 0x00) ? durationGoal : 0xffffffff;
|
||||
if (distanceGoal != 0xffffffff) {
|
||||
subTlv.put(0x07, distanceGoal);
|
||||
} else if (frameType == 0x01) {
|
||||
subTlv.put(0x06, stepGoal);
|
||||
}
|
||||
short durationGoalFinal = ((Type.data & 0x08) != 0x00) ? durationGoal : 0xffffffff;
|
||||
if (durationGoalFinal != 0xffffffff) {
|
||||
subTlv.put(0x08, durationGoalFinal);
|
||||
}
|
||||
HuaweiTLV containerTlv = new HuaweiTLV().put(0x82, subTlv);
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81, containerTlv);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static class MessageCount {
|
||||
public static final byte sleepId = 0x0C;
|
||||
public static final byte stepId = 0x0A;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(
|
||||
ParamsProvider paramsProvider,
|
||||
byte commandId,
|
||||
int start,
|
||||
int end
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = commandId;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81)
|
||||
.put(0x03, start)
|
||||
.put(0x04, end);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public short count;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() {
|
||||
this.count = this.tlv.getObject(0x81).getShort(0x02);
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MessageData {
|
||||
public static final byte sleepId = 0x0D;
|
||||
public static final byte stepId = 0x0B;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, byte commandId, short count) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = commandId;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81, new HuaweiTLV()
|
||||
.put(0x02, count)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SleepResponse extends HuaweiPacket {
|
||||
public static class SubContainer {
|
||||
public byte type;
|
||||
public byte[] timestamp;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SubContainer{" +
|
||||
"type=" + type +
|
||||
", timestamp=" + Arrays.toString(timestamp) +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public short number;
|
||||
public List<SubContainer> containers;
|
||||
|
||||
public SleepResponse(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = sleepId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() {
|
||||
HuaweiTLV container = this.tlv.getObject(0x81);
|
||||
List<HuaweiTLV> subContainers = container.getObjects(0x83);
|
||||
|
||||
this.number = container.getShort(0x02);
|
||||
this.containers = new ArrayList<>();
|
||||
for (HuaweiTLV subContainerTlv : subContainers) {
|
||||
SubContainer subContainer = new SubContainer();
|
||||
subContainer.type = subContainerTlv.getByte(0x04);
|
||||
subContainer.timestamp = subContainerTlv.getBytes(0x05);
|
||||
this.containers.add(subContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class StepResponse extends HuaweiPacket {
|
||||
public static class SubContainer {
|
||||
public static class TV {
|
||||
public final byte bitmap;
|
||||
public final byte tag;
|
||||
public final short value;
|
||||
|
||||
public TV(byte bitmap, byte tag, short value) {
|
||||
this.bitmap = bitmap;
|
||||
this.tag = tag;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TV{" +
|
||||
"bitmap=" + bitmap +
|
||||
", tag=" + tag +
|
||||
", value=" + value +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Data directly from packet
|
||||
*/
|
||||
public byte timestampOffset;
|
||||
public byte[] data;
|
||||
|
||||
/*
|
||||
* Inferred data
|
||||
*/
|
||||
public int timestamp;
|
||||
|
||||
public List<TV> parsedData = null;
|
||||
public String parsedDataError = "";
|
||||
|
||||
public int steps = -1;
|
||||
public int calories = -1;
|
||||
public int distance = -1;
|
||||
public int heartrate = -1;
|
||||
|
||||
public int spo = -1;
|
||||
|
||||
public List<TV> unknownTVs = null;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SubContainer{" +
|
||||
"timestampOffset=" + timestampOffset +
|
||||
", data=" + Arrays.toString(data) +
|
||||
", timestamp=" + timestamp +
|
||||
", parsedData=" + parsedData +
|
||||
", parsedDataError='" + parsedDataError + '\'' +
|
||||
", steps=" + steps +
|
||||
", calories=" + calories +
|
||||
", distance=" + distance +
|
||||
", spo=" + spo +
|
||||
", unknownTVs=" + unknownTVs +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public short number;
|
||||
public int timestamp;
|
||||
public List<SubContainer> containers;
|
||||
|
||||
private static final List<Byte> singleByteTagListBitmap1 = new ArrayList<>();
|
||||
static {
|
||||
singleByteTagListBitmap1.add((byte) 0x20);
|
||||
singleByteTagListBitmap1.add((byte) 0x40);
|
||||
}
|
||||
|
||||
public StepResponse(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = stepId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
HuaweiTLV container = this.tlv.getObject(0x81);
|
||||
List<HuaweiTLV> subContainers = container.getObjects(0x84);
|
||||
|
||||
if (!container.contains(0x02))
|
||||
throw new MissingTagException(0x02);
|
||||
if (!container.contains(0x03))
|
||||
throw new MissingTagException(0x03);
|
||||
|
||||
this.number = container.getShort(0x02);
|
||||
this.timestamp = container.getInteger(0x03);
|
||||
this.containers = new ArrayList<>();
|
||||
for (HuaweiTLV subContainerTlv : subContainers) {
|
||||
SubContainer subContainer = new SubContainer();
|
||||
subContainer.timestampOffset = subContainerTlv.getByte(0x05);
|
||||
subContainer.timestamp = this.timestamp + 60 * subContainer.timestampOffset;
|
||||
subContainer.data = subContainerTlv.getBytes(0x06);
|
||||
parseData(subContainer, subContainer.data);
|
||||
this.containers.add(subContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseData(SubContainer returnValue, byte[] data) {
|
||||
int i = 0;
|
||||
|
||||
if (data.length <= 0) {
|
||||
returnValue.parsedData = null;
|
||||
returnValue.parsedDataError = "Data is missing feature bitmap.";
|
||||
return;
|
||||
}
|
||||
byte featureBitmap1 = data[i++];
|
||||
|
||||
byte featureBitmap2 = 0;
|
||||
if ((featureBitmap1 & 128) != 0) {
|
||||
if (data.length <= i) {
|
||||
returnValue.parsedData = null;
|
||||
returnValue.parsedDataError = "Data is missing second feature bitmap.";
|
||||
return;
|
||||
}
|
||||
featureBitmap2 = data[i++];
|
||||
}
|
||||
|
||||
returnValue.parsedData = new ArrayList<>();
|
||||
returnValue.unknownTVs = new ArrayList<>();
|
||||
|
||||
// The greater than zero check is because Java is always signed, so we only check 7 bits
|
||||
for (byte bitToCheck = 1; bitToCheck > 0; bitToCheck <<= 1) {
|
||||
if ((featureBitmap1 & bitToCheck) != 0) {
|
||||
short value;
|
||||
|
||||
if (singleByteTagListBitmap1.contains(bitToCheck)) {
|
||||
if (data.length - 1 < i) {
|
||||
returnValue.parsedData = null;
|
||||
returnValue.parsedDataError = "Data is too short for selected features.";
|
||||
return;
|
||||
}
|
||||
|
||||
value = data[i++];
|
||||
} else {
|
||||
if (data.length - 2 < i) {
|
||||
returnValue.parsedData = null;
|
||||
returnValue.parsedDataError = "Data is too short for selected features.";
|
||||
return;
|
||||
}
|
||||
|
||||
value = (short) ((data[i++] & 0xFF) << 8 | (data[i++] & 0xFF));
|
||||
}
|
||||
|
||||
// The bitToCheck is used as tag, which may not be optimal, but works
|
||||
SubContainer.TV tv = new SubContainer.TV((byte) 1, bitToCheck, value);
|
||||
returnValue.parsedData.add(tv);
|
||||
|
||||
if (bitToCheck == 0x02)
|
||||
returnValue.steps = value;
|
||||
else if (bitToCheck == 0x04)
|
||||
returnValue.calories = value;
|
||||
else if (bitToCheck == 0x08)
|
||||
returnValue.distance = value;
|
||||
else if (bitToCheck == 0x40)
|
||||
returnValue.heartrate = value;
|
||||
else
|
||||
returnValue.unknownTVs.add(tv);
|
||||
}
|
||||
}
|
||||
|
||||
if (featureBitmap2 != 0) {
|
||||
// We want to check 8 bits here, and java is java, so we use a short
|
||||
for (short bitToCheck = 1; bitToCheck < 0x0100; bitToCheck <<= 1) {
|
||||
if ((featureBitmap2 & bitToCheck) != 0) {
|
||||
if (data.length - 1 < i) {
|
||||
returnValue.parsedData = null;
|
||||
returnValue.parsedDataError = "Data is too short for selected features.";
|
||||
return;
|
||||
}
|
||||
|
||||
byte value = data[i++];
|
||||
|
||||
SubContainer.TV tv = new SubContainer.TV((byte) 2, (byte) bitToCheck, value);
|
||||
returnValue.parsedData.add(tv);
|
||||
|
||||
if (bitToCheck == 0x01)
|
||||
returnValue.spo = value;
|
||||
else
|
||||
returnValue.unknownTVs.add(tv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class FitnessTotals {
|
||||
public static final byte id = 0x03;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public int totalSteps = 0;
|
||||
public int totalCalories = 0;
|
||||
public int totalDistance = 0;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() {
|
||||
HuaweiTLV container = this.tlv.getObject(0x81);
|
||||
List<HuaweiTLV> containers = container.getObjects(0x83);
|
||||
|
||||
for (HuaweiTLV tlv : containers) {
|
||||
if (tlv.contains(0x05))
|
||||
totalSteps += tlv.getInteger(0x05);
|
||||
if (tlv.contains(0x06))
|
||||
totalCalories += tlv.getShort(0x06);
|
||||
if (tlv.contains(0x07))
|
||||
totalDistance += tlv.getInteger(0x07);
|
||||
}
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ActivityReminder {
|
||||
public static final byte id = 0x07;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(
|
||||
ParamsProvider paramsProvider,
|
||||
boolean longSitSwitch,
|
||||
byte longSitInterval,
|
||||
byte[] longSitStart,
|
||||
byte[] longSitEnd,
|
||||
byte cycle
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81, new HuaweiTLV()
|
||||
.put(0x02, longSitSwitch)
|
||||
.put(0x03, longSitInterval)
|
||||
.put(0x04, longSitStart)
|
||||
.put(0x05, longSitEnd)
|
||||
.put(0x06, cycle)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TruSleep {
|
||||
public static final byte id = 0x16;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, boolean truSleepSwitch) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, truSleepSwitch);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class EnableAutomaticHeartrate {
|
||||
public static final byte id = 0x17;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, boolean enableAutomaticHeartrate) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, enableAutomaticHeartrate);
|
||||
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotifyRestHeartRate {
|
||||
public static final byte id = 0x23;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, 0x01);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class EnableAutomaticSpo {
|
||||
public static final byte id = 0x24;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, boolean enableAutomaticSpo) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, enableAutomaticSpo);
|
||||
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MediumToStrengthThreshold {
|
||||
public static final byte id = 0x23;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider,
|
||||
byte walkRun,
|
||||
byte climb,
|
||||
byte heartRate,
|
||||
byte cycleSpeed,
|
||||
byte sample,
|
||||
byte countLength) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
if (walkRun < 0x00 || walkRun > 0xc8) walkRun = 0x6E;
|
||||
if (climb < 0x0 || climb > 0xc8) climb = 0x3c;
|
||||
if (heartRate < 0x0 || heartRate > 0x64) heartRate = 0x40;
|
||||
if (cycleSpeed < 0x0 || cycleSpeed > 0xff) cycleSpeed = 0x50;
|
||||
if (sample < 0x1 || sample > 0xa) sample = 0x3;
|
||||
if (countLength < 0x1 || countLength > 0xa) countLength = 0x5;
|
||||
if (countLength < sample) countLength = sample;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, walkRun)
|
||||
.put(0x02, climb)
|
||||
.put(0x03, heartRate)
|
||||
.put(0x04, cycleSpeed)
|
||||
.put(0x05, sample)
|
||||
.put(0x06, countLength);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Type {
|
||||
public static final byte goal = 0x01;
|
||||
public static final byte motion = 0x00;
|
||||
public static final byte data = 0x01;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class LocaleConfig {
|
||||
public static final byte id = 0x0C;
|
||||
|
||||
public static class SetLanguageSetting extends HuaweiPacket {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public SetLanguageSetting(
|
||||
ParamsProvider paramsProvider,
|
||||
byte[] locale,
|
||||
byte measurement
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = LocaleConfig.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, locale)
|
||||
.put(0x02, measurement);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MeasurementSystem {
|
||||
// TODO: enum?
|
||||
|
||||
public static final byte metric = 0x00;
|
||||
public static final byte imperial = 0x01;
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class Menstrual {
|
||||
public static final byte id = 0x32;
|
||||
|
||||
public static class ModifyTime {
|
||||
public static final byte id = 0x02;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, int errorCode, long time) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Menstrual.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV();
|
||||
if (errorCode == 0) {
|
||||
this.tlv.put(0x01, time);
|
||||
} else {
|
||||
this.tlv.put(0x7f, (int)0x249F1);
|
||||
}
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Menstrual.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
// Do not know data yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class CapabilityRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x05;
|
||||
|
||||
public CapabilityRequest(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Menstrual.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, (byte)0x02);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class MusicControl {
|
||||
public static final byte id = 0x25;
|
||||
|
||||
// TODO: should this be in HuaweiConstants?
|
||||
public static final int successValue = 0x000186A0;
|
||||
|
||||
public static class MusicStatusRequest extends HuaweiPacket {
|
||||
public MusicStatusRequest(ParamsProvider paramsProvider, byte commandId, int returnValue) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = commandId;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x7F, returnValue);
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicStatusResponse extends HuaweiPacket {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public int status = -1;
|
||||
|
||||
public MusicStatusResponse(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() {
|
||||
if (this.tlv.contains(0x7F) && this.tlv.getBytes(0x7F).length == 4)
|
||||
this.status = this.tlv.getInteger(0x7F);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicInfo {
|
||||
public static final byte id = 0x02;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(
|
||||
ParamsProvider paramsProvider,
|
||||
String artistName,
|
||||
String songName,
|
||||
byte playState,
|
||||
byte maxVolume,
|
||||
byte currentVolume
|
||||
) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, artistName)
|
||||
.put(0x02, songName)
|
||||
.put(0x03, playState)
|
||||
.put(0x04, maxVolume)
|
||||
.put(0x05, currentVolume);
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public boolean ok = false;
|
||||
public String error = "No input has been parsed yet";
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.isEncrypted = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() {
|
||||
if (this.tlv.contains(0x7F)) {
|
||||
if (this.tlv.getInteger(0x7F) == successValue) {
|
||||
this.ok = true;
|
||||
this.error = "";
|
||||
} else {
|
||||
this.ok = false;
|
||||
this.error = "Music information error code: " + Integer.toHexString(this.tlv.getInteger(0x7F));
|
||||
}
|
||||
} else {
|
||||
this.ok = false;
|
||||
this.error = "Music information response no status tag";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Control {
|
||||
public static final byte id = 0x03;
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public enum Button {
|
||||
Unknown,
|
||||
Play,
|
||||
Pause,
|
||||
Previous,
|
||||
Next,
|
||||
Volume_up,
|
||||
Volume_down
|
||||
}
|
||||
|
||||
public boolean buttonPresent = false;
|
||||
public byte rawButton = 0x00;
|
||||
public boolean volumePresent = false;
|
||||
public byte volume = 0x00;
|
||||
|
||||
public Button button = null;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.isEncrypted = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() {
|
||||
if (this.tlv.contains(0x01)) {
|
||||
this.buttonPresent = true;
|
||||
this.rawButton = this.tlv.getByte(0x01);
|
||||
switch (this.rawButton) {
|
||||
case 1:
|
||||
this.button = Button.Play;
|
||||
break;
|
||||
case 2:
|
||||
this.button = Button.Pause;
|
||||
break;
|
||||
case 3:
|
||||
this.button = Button.Previous;
|
||||
break;
|
||||
case 4:
|
||||
this.button = Button.Next;
|
||||
break;
|
||||
case 5:
|
||||
this.button = Button.Volume_up;
|
||||
break;
|
||||
case 6:
|
||||
this.button = Button.Volume_down;
|
||||
break;
|
||||
case 64:
|
||||
// Unknown button on Huawei Band 4
|
||||
default:
|
||||
this.button = Button.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.tlv.contains(0x02)) {
|
||||
this.volumePresent = true;
|
||||
this.volume = this.tlv.getByte(0x02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class Notifications {
|
||||
public static final byte id = 0x02;
|
||||
|
||||
public static class NotificationActionRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
// TODO: support other types of notifications
|
||||
// public static final int send = 0x01;
|
||||
// public static final int notificationId = 0x01;
|
||||
// public static final int notificationType = 0x02;
|
||||
// public static final int vibrate = 0x03;
|
||||
// public static final int payloadEmpty = 0x04;
|
||||
// public static final int imageHeight = 0x08;
|
||||
// public static final int imageWidth = 0x09;
|
||||
// public static final int imageColor = 0x0A;
|
||||
// public static final int imageData = 0x0B;
|
||||
// public static final int textType = 0x0E;
|
||||
// public static final int textEncoding = 0x0F;
|
||||
// public static final int textContent = 0x10;
|
||||
// public static final int sourceAppId = 0x11;
|
||||
// public static final int payloadText = 0x84;
|
||||
// public static final int payloadImage = 0x86;
|
||||
// public static final int textList = 0x8C;
|
||||
// public static final int textItem = 0x8D;
|
||||
|
||||
public NotificationActionRequest(
|
||||
ParamsProvider paramsProvider,
|
||||
short notificationId,
|
||||
byte notificationType,
|
||||
byte titleEncoding,
|
||||
String titleContent,
|
||||
byte senderEncoding,
|
||||
String senderContent,
|
||||
byte bodyEncoding,
|
||||
String bodyContent,
|
||||
String sourceAppId
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
|
||||
// TODO: Add notification information per type if necessary
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, notificationId)
|
||||
.put(0x02, notificationType)
|
||||
.put(0x03, true); // This used to be vibrate, but doesn't work
|
||||
|
||||
HuaweiTLV subTlv = new HuaweiTLV();
|
||||
if (titleContent != null)
|
||||
subTlv.put(0x8D, new HuaweiTLV()
|
||||
.put(0x0E, (byte) 0x03)
|
||||
.put(0x0F, titleEncoding)
|
||||
.put(0x10, titleContent)
|
||||
);
|
||||
|
||||
if (senderContent != null)
|
||||
subTlv.put(0x8D, new HuaweiTLV()
|
||||
.put(0x0E, (byte) 0x02)
|
||||
.put(0x0F, senderEncoding)
|
||||
.put(0x10, senderContent)
|
||||
);
|
||||
|
||||
if (bodyContent != null)
|
||||
subTlv.put(0x8D, new HuaweiTLV()
|
||||
.put(0x0E, (byte) 0x01)
|
||||
.put(0x0F, bodyEncoding)
|
||||
.put(0x10, bodyContent)
|
||||
);
|
||||
|
||||
if (subTlv.length() != 0) {
|
||||
this.tlv.put(0x84, new HuaweiTLV().put(0x8C, subTlv));
|
||||
} else {
|
||||
this.tlv.put(0x04);
|
||||
}
|
||||
|
||||
if (sourceAppId != null)
|
||||
this.tlv.put(0x11, sourceAppId);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotificationConstraints {
|
||||
public static final byte id = 0x02;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01);
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public ByteBuffer constraints;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
this.complete = true;
|
||||
}
|
||||
|
||||
private void putByteBuffer(ByteBuffer bBuffer, byte position, byte[] value) {
|
||||
ByteBuffer bValue = ByteBuffer.wrap(value);
|
||||
if (bValue.capacity() == 2)
|
||||
bBuffer.putShort(position, bValue.getShort());
|
||||
bBuffer.put(position, (byte)0x00);
|
||||
bBuffer.put(bValue.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
this.constraints = ByteBuffer.allocate(14);
|
||||
List<HuaweiTLV> subContainers = this.tlv
|
||||
.getObject(0x81)
|
||||
.getObject(0x82)
|
||||
.getObjects(0x90);
|
||||
for (HuaweiTLV subContainer : subContainers) {
|
||||
HuaweiTLV subSubContainer = subContainer.getObject(0x91);
|
||||
if (subSubContainer.getByte(0x12) == 0x01)
|
||||
putByteBuffer(constraints, NotificationConstraintsType.contentLength,subSubContainer.getBytes(0x14));
|
||||
if (subSubContainer.getByte(0x12) == 0x05) {
|
||||
constraints.put(NotificationConstraintsType.yellowPagesSupport,(byte)0x01);
|
||||
constraints.put(NotificationConstraintsType.yellowPagesFormat,subSubContainer.getByte(0x13));
|
||||
putByteBuffer(constraints, NotificationConstraintsType.yellowPagesLength,subSubContainer.getBytes(0x14));
|
||||
}
|
||||
if (subSubContainer.getByte(0x12) == 0x06) {
|
||||
constraints.put(NotificationConstraintsType.contentSignSupport,(byte)0x01);
|
||||
constraints.put(NotificationConstraintsType.contentSignFormat,subSubContainer.getByte(0x13));
|
||||
putByteBuffer(constraints, NotificationConstraintsType.contentSignLength,subSubContainer.getBytes(0x14));
|
||||
}
|
||||
if (subSubContainer.getByte(0x12) == 0x07 ) {
|
||||
constraints.put(NotificationConstraintsType.incomingNumberSupport,(byte)0x01);
|
||||
constraints.put(NotificationConstraintsType.incomingNumberFormat,subSubContainer.getByte(0x13));
|
||||
putByteBuffer(constraints, NotificationConstraintsType.incomingNumberLength,subSubContainer.getBytes(0x14));
|
||||
}
|
||||
}
|
||||
constraints.rewind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotificationConstraintsType {
|
||||
// TODO: enum?
|
||||
|
||||
public static final byte contentLength = 0x00;
|
||||
public static final byte yellowPagesSupport = 0x02;
|
||||
public static final byte yellowPagesFormat = 0x03;
|
||||
public static final byte yellowPagesLength = 0x04;
|
||||
public static final byte contentSignSupport = 0x06;
|
||||
public static final byte contentSignFormat = 0x07;
|
||||
public static final byte contentSignLength = 0x08;
|
||||
public static final byte incomingNumberSupport = 0x0A;
|
||||
public static final byte incomingNumberFormat = 0x0B;
|
||||
public static final byte incomingNumberLength = 0x0C;
|
||||
}
|
||||
|
||||
public static class NotificationType {
|
||||
// TODO: enum?
|
||||
|
||||
public static final byte call = 0x01;
|
||||
public static final byte sms = 0x02;
|
||||
public static final byte weChat = 0x03;
|
||||
public static final byte qq = 0x0B;
|
||||
public static final byte stopNotification = 0x0C; // To stop showing a (call) notification
|
||||
public static final byte missedCall = 0x0E;
|
||||
public static final byte email = 0x0F;
|
||||
public static final byte generic = 0x7F;
|
||||
}
|
||||
|
||||
public static class TextType {
|
||||
// TODO: enum?
|
||||
|
||||
public static final int text = 0x01;
|
||||
public static final int sender = 0x02;
|
||||
public static final int title = 0x03;
|
||||
public static final int yellowPage = 0x05;
|
||||
public static final int contentSign = 0x06;
|
||||
public static final int flight = 0x07;
|
||||
public static final int train = 0x08;
|
||||
public static final int warmRemind = 0x09;
|
||||
public static final int weather = 0x0A;
|
||||
}
|
||||
|
||||
public static class TextEncoding {
|
||||
// TODO: enum?
|
||||
|
||||
public static final byte unknown = 0x01;
|
||||
public static final byte standard = 0x02;
|
||||
}
|
||||
|
||||
public static class NotificationStateRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x04;
|
||||
|
||||
public NotificationStateRequest(
|
||||
ParamsProvider paramsProvider,
|
||||
boolean status
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81, new HuaweiTLV()
|
||||
.put(0x02, status)
|
||||
.put(0x03, status)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotificationCapabilities {
|
||||
public static final byte id = 0x05;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(
|
||||
ParamsProvider paramsProvider
|
||||
){
|
||||
super(paramsProvider);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public byte capabilities = 0x00;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (this.tlv.contains(0x01))
|
||||
this.capabilities = this.tlv.getByte(0x01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WearMessagePushRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x08;
|
||||
|
||||
public WearMessagePushRequest(
|
||||
ParamsProvider paramsProvider,
|
||||
boolean status
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
|
||||
/* Value sent is the opposite of the switch status */
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, !status);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class WorkMode {
|
||||
public static final byte id = 0x26;
|
||||
|
||||
/*
|
||||
* public static class ModeStatus {
|
||||
* public static final byte id = 0x01;
|
||||
* public static final int autoDetectMode = 0x01;
|
||||
* public static final int footWear = 0x02;
|
||||
* }
|
||||
*/
|
||||
|
||||
public static class SwitchStatusRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x02;
|
||||
public static final int setStatus = 0x01;
|
||||
|
||||
public SwitchStatusRequest(ParamsProvider paramsProvider, boolean autoWorkMode) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = WorkMode.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, autoWorkMode);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* public static class FootWear {
|
||||
* public static final byte id = 0x03;
|
||||
* public static final int AutoDetectMode = 0x01;
|
||||
* public static final int FootWear = 0x02;
|
||||
* }
|
||||
*/
|
||||
}
|
@ -0,0 +1,575 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.packets;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class Workout {
|
||||
public static final byte id = 0x17;
|
||||
|
||||
public static class WorkoutCount {
|
||||
public static final byte id = 0x07;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(
|
||||
ParamsProvider paramsProvider,
|
||||
int start,
|
||||
int end
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81, new HuaweiTLV()
|
||||
.put(0x03, start)
|
||||
.put(0x04, end)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public static class WorkoutNumbers {
|
||||
public byte[] rawData;
|
||||
|
||||
public short workoutNumber;
|
||||
public short dataCount;
|
||||
public short paceCount;
|
||||
}
|
||||
|
||||
public short count;
|
||||
public List<WorkoutNumbers> workoutNumbers;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x81))
|
||||
throw new MissingTagException(0x81);
|
||||
|
||||
HuaweiTLV container = this.tlv.getObject(0x81);
|
||||
|
||||
if (!container.contains(0x02))
|
||||
throw new MissingTagException(0x02);
|
||||
|
||||
this.count = container.getShort(0x02);
|
||||
this.workoutNumbers = new ArrayList<>();
|
||||
|
||||
if (this.count == 0)
|
||||
return;
|
||||
|
||||
if (!container.contains(0x85))
|
||||
throw new MissingTagException(0x85);
|
||||
|
||||
List<HuaweiTLV> subContainers = container.getObjects(0x85);
|
||||
for (HuaweiTLV subContainerTlv : subContainers) {
|
||||
if (!subContainerTlv.contains(0x06))
|
||||
throw new MissingTagException(0x06);
|
||||
if (!subContainerTlv.contains(0x07))
|
||||
throw new MissingTagException(0x07);
|
||||
if (!subContainerTlv.contains(0x08))
|
||||
throw new MissingTagException(0x08);
|
||||
|
||||
WorkoutNumbers workoutNumber = new WorkoutNumbers();
|
||||
workoutNumber.rawData = subContainerTlv.serialize();
|
||||
workoutNumber.workoutNumber = subContainerTlv.getShort(0x06);
|
||||
workoutNumber.dataCount = subContainerTlv.getShort(0x07);
|
||||
workoutNumber.paceCount = subContainerTlv.getShort(0x08);
|
||||
this.workoutNumbers.add(workoutNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WorkoutTotals {
|
||||
public static final byte id = 0x08;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
|
||||
public Request(ParamsProvider paramsProvider, short number) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV()
|
||||
.put(0x02, number)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public byte[] rawData;
|
||||
|
||||
public short number;
|
||||
public byte status = -1; // TODO: enum?
|
||||
public int startTime;
|
||||
public int endTime;
|
||||
public int calories = -1;
|
||||
public int distance = -1;
|
||||
public int stepCount = -1;
|
||||
public int totalTime = -1;
|
||||
public int duration = -1;
|
||||
public byte type = -1; // TODO: enum?
|
||||
public short strokes = -1;
|
||||
public short avgStrokeRate = -1;
|
||||
public short poolLength = -1; // In cm
|
||||
public short laps = -1;
|
||||
public short avgSwolf = -1;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x81))
|
||||
throw new MissingTagException(0x81);
|
||||
|
||||
HuaweiTLV container = this.tlv.getObject(0x81);
|
||||
|
||||
if (!container.contains(0x02))
|
||||
throw new MissingTagException(0x02);
|
||||
if (!container.contains(0x04))
|
||||
throw new MissingTagException(0x04);
|
||||
if (!container.contains(0x05))
|
||||
throw new MissingTagException(0x05);
|
||||
|
||||
this.rawData = container.serialize();
|
||||
this.number = container.getShort(0x02);
|
||||
if (container.contains(0x03))
|
||||
this.status = container.getByte(0x03);
|
||||
this.startTime = container.getInteger(0x04);
|
||||
this.endTime = container.getInteger(0x05);
|
||||
|
||||
if (container.contains(0x06))
|
||||
this.calories = container.getInteger(0x06);
|
||||
if (container.contains(0x07))
|
||||
this.distance = container.getInteger(0x07);
|
||||
if (container.contains(0x08))
|
||||
this.stepCount = container.getInteger(0x08);
|
||||
if (container.contains(0x09))
|
||||
this.totalTime = container.getInteger(0x09);
|
||||
if (container.contains(0x12))
|
||||
this.duration = container.getInteger(0x12);
|
||||
if (container.contains(0x14))
|
||||
this.type = container.getByte(0x14);
|
||||
// TODO: I'm guessing 0x15 is Main style for swimming, but cannot confirm.
|
||||
if (container.contains(0x16))
|
||||
this.strokes = container.getShort(0x16);
|
||||
if (container.contains(0x17))
|
||||
this.avgStrokeRate = container.getShort(0x17);
|
||||
if (container.contains(0x18))
|
||||
this.poolLength = container.getShort(0x18);
|
||||
if (container.contains(0x19))
|
||||
this.laps = container.getShort(0x19);
|
||||
if (container.contains(0x1a))
|
||||
this.avgSwolf = container.getShort(0x1a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WorkoutData {
|
||||
public static final int id = 0x0a;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
|
||||
public Request(
|
||||
ParamsProvider paramsProvider,
|
||||
short workoutNumber,
|
||||
short dataNumber
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV()
|
||||
.put(0x02, workoutNumber)
|
||||
.put(0x03, dataNumber)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public static class Header {
|
||||
public short workoutNumber;
|
||||
public short dataNumber;
|
||||
public int timestamp;
|
||||
public byte interval;
|
||||
public short dataCount;
|
||||
public byte dataLength;
|
||||
public short bitmap; // TODO: can this be enum-like?
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Header{" +
|
||||
"workoutNumber=" + workoutNumber +
|
||||
", dataNumber=" + dataNumber +
|
||||
", timestamp=" + timestamp +
|
||||
", interval=" + interval +
|
||||
", dataCount=" + dataCount +
|
||||
", dataLength=" + dataLength +
|
||||
", bitmap=" + bitmap +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
// If unknown data is encountered, the whole tlv will be in here so it can be parsed again later
|
||||
public byte[] unknownData = null;
|
||||
|
||||
public byte heartRate = -1;
|
||||
public short speed = -1;
|
||||
public byte stepRate = -1;
|
||||
|
||||
public short cadence = -1;
|
||||
public short stepLength = -1;
|
||||
public short groundContactTime = -1;
|
||||
public byte impact = -1;
|
||||
public short swingAngle = -1;
|
||||
public byte foreFootLanding = -1;
|
||||
public byte midFootLanding = -1;
|
||||
public byte backFootLanding = -1;
|
||||
public byte eversionAngle = -1;
|
||||
|
||||
public byte swolf = -1;
|
||||
public short strokeRate = -1;
|
||||
|
||||
public int timestamp = -1; // Calculated timestamp for this data point
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Data{" +
|
||||
"unknownData=" + unknownData +
|
||||
", heartRate=" + heartRate +
|
||||
", speed=" + speed +
|
||||
", stepRate=" + stepRate +
|
||||
", cadence=" + cadence +
|
||||
", stepLength=" + stepLength +
|
||||
", groundContactTime=" + groundContactTime +
|
||||
", impact=" + impact +
|
||||
", swingAngle=" + swingAngle +
|
||||
", foreFootLanding=" + foreFootLanding +
|
||||
", midFootLanding=" + midFootLanding +
|
||||
", backFootLanding=" + backFootLanding +
|
||||
", eversionAngle=" + eversionAngle +
|
||||
", swolf=" + swolf +
|
||||
", strokeRate=" + strokeRate +
|
||||
", timestamp=" + timestamp +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: I'm not sure about the lengths
|
||||
private final byte[] bitmapLengths = {1, 2, 1, 2, 2, 4, -1, 2, 2, 1, 1, 1, 1, 1, 1, 1};
|
||||
private final byte[] innerBitmapLengths = {2, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1};
|
||||
|
||||
public short workoutNumber;
|
||||
public short dataNumber;
|
||||
public byte[] rawHeader;
|
||||
public byte[] rawData;
|
||||
public short innerBitmap;
|
||||
|
||||
public Header header;
|
||||
public List<Data> dataList;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is to be able to easily reparse the error data, only accepts tlv bytes
|
||||
* @param rawData The TLV bytes
|
||||
*/
|
||||
public Response(byte[] rawData) throws ParseException {
|
||||
super(null);
|
||||
this.tlv = new HuaweiTLV().parse(rawData);
|
||||
this.parseTlv();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x81))
|
||||
throw new MissingTagException(0x81);
|
||||
|
||||
HuaweiTLV container = this.tlv.getObject(0x81);
|
||||
|
||||
if (!container.contains(0x02))
|
||||
throw new MissingTagException(0x02);
|
||||
if (!container.contains(0x03))
|
||||
throw new MissingTagException(0x03);
|
||||
if (!container.contains(0x04))
|
||||
throw new MissingTagException(0x04);
|
||||
if (!container.contains(0x05))
|
||||
throw new MissingTagException(0x05); // TODO: not sure if 5 can also be omitted
|
||||
|
||||
this.workoutNumber = container.getShort(0x02);
|
||||
this.dataNumber = container.getShort(0x03);
|
||||
this.rawHeader = container.getBytes(0x04);
|
||||
this.rawData = container.getBytes(0x05);
|
||||
|
||||
if (container.contains(0x09))
|
||||
innerBitmap = container.getShort(0x09);
|
||||
else
|
||||
innerBitmap = 0x01FF; // This seems to be the default
|
||||
|
||||
int innerDataLength = 0;
|
||||
for (byte i = 0; i < 16; i++) {
|
||||
if ((innerBitmap & (1 << i)) != 0) {
|
||||
innerDataLength += innerBitmapLengths[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.rawHeader.length != 14)
|
||||
throw new LengthMismatchException("Workout data header length mismatch.");
|
||||
|
||||
this.header = new Header();
|
||||
ByteBuffer buf = ByteBuffer.wrap(this.rawHeader);
|
||||
header.workoutNumber = buf.getShort();
|
||||
header.dataNumber = buf.getShort();
|
||||
header.timestamp = buf.getInt();
|
||||
header.interval = buf.get();
|
||||
header.dataCount = buf.getShort();
|
||||
header.dataLength = buf.get();
|
||||
header.bitmap = buf.getShort();
|
||||
|
||||
// Check data lengths from header
|
||||
if (this.header.dataCount * this.header.dataLength != this.rawData.length)
|
||||
throw new LengthMismatchException("Workout data length mismatch with header.");
|
||||
|
||||
// Check data lengths from bitmap
|
||||
int dataLength = 0;
|
||||
for (byte i = 0; i < 16; i++) {
|
||||
if ((header.bitmap & (1 << i)) != 0) {
|
||||
if (i == 6) {
|
||||
dataLength += innerDataLength;
|
||||
} else {
|
||||
dataLength += bitmapLengths[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
dataLength = dataLength * header.dataCount;
|
||||
if (dataLength != this.rawData.length)
|
||||
throw new LengthMismatchException("Workout data length mismatch with bitmap.");
|
||||
|
||||
this.dataList = new ArrayList<>();
|
||||
buf = ByteBuffer.wrap(this.rawData);
|
||||
for (short i = 0; i < header.dataCount; i++) {
|
||||
Data data = new Data();
|
||||
data.timestamp = header.timestamp + header.interval * i;
|
||||
for (byte j = 0; j < 16; j++) {
|
||||
if ((header.bitmap & (1 << j)) != 0) {
|
||||
switch (j) {
|
||||
case 0:
|
||||
data.heartRate = buf.get();
|
||||
break;
|
||||
case 1:
|
||||
data.speed = buf.getShort();
|
||||
break;
|
||||
case 2:
|
||||
data.stepRate = buf.get();
|
||||
break;
|
||||
case 3:
|
||||
data.swolf = buf.get();
|
||||
break;
|
||||
case 4:
|
||||
data.strokeRate = buf.getShort();
|
||||
break;
|
||||
case 6:
|
||||
// Inner data, parsing into data
|
||||
// TODO: function for readability?
|
||||
for (byte k = 0; k < 16; k++) {
|
||||
if ((innerBitmap & (1 << k)) != 0) {
|
||||
switch (k) {
|
||||
case 0:
|
||||
data.cadence = buf.getShort();
|
||||
break;
|
||||
case 1:
|
||||
data.stepLength = buf.getShort();
|
||||
break;
|
||||
case 2:
|
||||
data.groundContactTime = buf.getShort();
|
||||
break;
|
||||
case 3:
|
||||
data.impact = buf.get();
|
||||
break;
|
||||
case 4:
|
||||
data.swingAngle = buf.getShort();
|
||||
break;
|
||||
case 5:
|
||||
data.foreFootLanding = buf.get();
|
||||
break;
|
||||
case 6:
|
||||
data.midFootLanding = buf.get();
|
||||
break;
|
||||
case 7:
|
||||
data.backFootLanding = buf.get();
|
||||
break;
|
||||
case 8:
|
||||
data.eversionAngle = buf.get();
|
||||
break;
|
||||
default:
|
||||
data.unknownData = this.tlv.serialize();
|
||||
// Fix alignment
|
||||
for (int l = 0; l < innerBitmapLengths[k]; l++)
|
||||
buf.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
data.unknownData = this.tlv.serialize();
|
||||
// Fix alignment
|
||||
for (int k = 0; k < bitmapLengths[j]; k++)
|
||||
buf.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dataList.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WorkoutPace {
|
||||
public static final int id = 0x0c;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
|
||||
public Request(
|
||||
ParamsProvider paramsProvider,
|
||||
short workoutNumber,
|
||||
short paceNumber
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV()
|
||||
.put(0x02, workoutNumber)
|
||||
.put(0x08, paceNumber)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public static class Block {
|
||||
public short distance = -1;
|
||||
public byte type = -1;
|
||||
public int pace = -1;
|
||||
public short correction = 0;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Block{" +
|
||||
"distance=" + distance +
|
||||
", type=" + type +
|
||||
", pace=" + pace +
|
||||
", correction=" + correction +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public short workoutNumber;
|
||||
public short paceNumber;
|
||||
public List<Block> blocks;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x81))
|
||||
throw new MissingTagException(0x81);
|
||||
|
||||
HuaweiTLV container = this.tlv.getObject(0x81);
|
||||
|
||||
if (!container.contains(0x02))
|
||||
throw new MissingTagException(0x02);
|
||||
if (!container.contains(0x08))
|
||||
throw new MissingTagException(0x08);
|
||||
// TODO: not sure what happens with an empty workout here...
|
||||
if (!container.contains(0x83))
|
||||
throw new MissingTagException(0x83);
|
||||
|
||||
this.workoutNumber = container.getShort(0x02);
|
||||
this.paceNumber = container.getShort(0x08);
|
||||
|
||||
this.blocks = new ArrayList<>();
|
||||
for (HuaweiTLV blockTlv : container.getObjects(0x83)) {
|
||||
if (!blockTlv.contains(0x04))
|
||||
throw new MissingTagException(0x04);
|
||||
if (!blockTlv.contains(0x05))
|
||||
throw new MissingTagException(0x05);
|
||||
if (!blockTlv.contains(0x06))
|
||||
throw new MissingTagException(0x06);
|
||||
|
||||
Block block = new Block();
|
||||
block.distance = blockTlv.getShort(0x04);
|
||||
block.type = blockTlv.getByte(0x05);
|
||||
block.pace = blockTlv.getInteger(0x06);
|
||||
if (blockTlv.contains(0x09))
|
||||
block.correction = blockTlv.getShort(0x09);
|
||||
blocks.add(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotifyHeartRate {
|
||||
public static final int id = 0x17;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x01, 0x03);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -98,6 +98,21 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband5.MiBand5Coordin
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband6.MiBand6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband7.MiBand7Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppe.ZeppECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband3.HonorBand3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband4.HonorBand4Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband5.HonorBand5Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband6.HonorBand6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband7.HonorBand7Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband4pro.HuaweiBand4ProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband6.HuaweiBand6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband7.HuaweiBand7Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband8.HuaweiBand8Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweibandaw70.HuaweiBandAw70Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweitalkbandb6.HuaweiTalkBandB6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt.HuaweiWatchGTCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt2.HuaweiWatchGT2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt2e.HuaweiWatchGT2eCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt3.HuaweiWatchGT3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.itag.ITagCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16DeviceCoordinator;
|
||||
@ -296,6 +311,21 @@ public enum DeviceType {
|
||||
SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class),
|
||||
SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class),
|
||||
BOSE_QC35(QC35Coordinator.class),
|
||||
HONORBAND3(HonorBand3Coordinator.class),
|
||||
HONORBAND4(HonorBand4Coordinator.class),
|
||||
HONORBAND5(HonorBand5Coordinator.class),
|
||||
HUAWEIBANDAW70(HuaweiBandAw70Coordinator.class),
|
||||
HUAWEIBAND6(HuaweiBand6Coordinator.class),
|
||||
HUAWEIWATCHGT(HuaweiWatchGTCoordinator.class),
|
||||
HUAWEIBAND4PRO(HuaweiBand4ProCoordinator.class),
|
||||
HUAWEIWATCHGT2(HuaweiWatchGT2Coordinator.class),
|
||||
HUAWEIWATCHGT2E(HuaweiWatchGT2eCoordinator.class),
|
||||
HUAWEITALKBANDB6(HuaweiTalkBandB6Coordinator.class),
|
||||
HUAWEIBAND7(HuaweiBand7Coordinator.class),
|
||||
HONORBAND6(HonorBand6Coordinator.class),
|
||||
HONORBAND7(HonorBand7Coordinator.class),
|
||||
HUAWEIWATCHGT3(HuaweiWatchGT3Coordinator.class),
|
||||
HUAWEIBAND8(HuaweiBand8Coordinator.class),
|
||||
VESC(VescCoordinator.class),
|
||||
BINARY_SENSOR(BinarySensorCoordinator.class),
|
||||
FLIPPER_ZERO(FlipperZeroCoordinator.class),
|
||||
|
@ -140,6 +140,9 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
|
||||
initializeDevice(createTransactionBuilder("Initializing device")).queue(getQueue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindPhone(boolean start) {}
|
||||
|
||||
@Override
|
||||
public void onSetFmFrequency(float frequency) {}
|
||||
|
||||
|
@ -0,0 +1,378 @@
|
||||
/* Copyright (C) 2022-2023 Martin.JM
|
||||
Copyright (C) 2022-2023 Gaignon Damien
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalTime;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicStatusRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* Handles responses that are not a reply to a request
|
||||
*
|
||||
*/
|
||||
public class AsynchronousResponse {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AsynchronousResponse.class);
|
||||
|
||||
private final HuaweiSupportProvider support;
|
||||
private final Handler mFindPhoneHandler = new Handler();
|
||||
private final static HashMap<Integer, String> dayOfWeekMap = new HashMap<>();
|
||||
static {
|
||||
dayOfWeekMap.put(Calendar.MONDAY, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_MO);
|
||||
dayOfWeekMap.put(Calendar.TUESDAY, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_TU);
|
||||
dayOfWeekMap.put(Calendar.WEDNESDAY, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_WE);
|
||||
dayOfWeekMap.put(Calendar.THURSDAY, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_TH);
|
||||
dayOfWeekMap.put(Calendar.FRIDAY, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_FR);
|
||||
dayOfWeekMap.put(Calendar.SATURDAY, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SA);
|
||||
dayOfWeekMap.put(Calendar.SUNDAY, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SU);
|
||||
}
|
||||
|
||||
public AsynchronousResponse(HuaweiSupportProvider support) {
|
||||
this.support = support;
|
||||
}
|
||||
|
||||
public void handleResponse(HuaweiPacket response) {
|
||||
try {
|
||||
response.parseTlv();
|
||||
} catch (HuaweiPacket.ParseException e) {
|
||||
LOG.error("Parse TLV exception", e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
handleFindPhone(response);
|
||||
handleMusicControls(response);
|
||||
handleCallControls(response);
|
||||
handlePhoneInfo(response);
|
||||
handleMenstrualModifyTime(response);
|
||||
} catch (Request.ResponseParseException e) {
|
||||
LOG.error("Response parse exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFindPhone(HuaweiPacket response) throws Request.ResponseParseException {
|
||||
if (response.serviceId == FindPhone.id && response.commandId == FindPhone.Response.id) {
|
||||
if (!(response instanceof FindPhone.Response))
|
||||
throw new Request.ResponseTypeMismatchException(response, FindPhone.Response.class);
|
||||
|
||||
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(support.getDeviceMac());
|
||||
|
||||
String findPhone = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_FIND_PHONE, support.getContext().getString(R.string.p_off));
|
||||
|
||||
if (findPhone.equals(support.getContext().getString(R.string.p_off))) {
|
||||
LOG.debug("Find phone command received, but it is disabled");
|
||||
// TODO: hide applet on device
|
||||
return;
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean("disable_find_phone_with_dnd", false) && dndActive()) {
|
||||
LOG.debug("Find phone command received, ringing prevented because of DND");
|
||||
// TODO: stop the band from showing as ringing
|
||||
return;
|
||||
}
|
||||
|
||||
if (!findPhone.equals(support.getContext().getString(R.string.p_on))) {
|
||||
// Duration set, stop after specified time
|
||||
String strDuration = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_FIND_PHONE_DURATION, "0");
|
||||
|
||||
int duration = Integer.parseInt(strDuration);
|
||||
if (duration > 0) {
|
||||
mFindPhoneHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
|
||||
support.evaluateGBDeviceEvent(findPhoneEvent);
|
||||
|
||||
// TODO: stop the band from showing as ringing
|
||||
}
|
||||
}, duration * 1000L);
|
||||
}
|
||||
}
|
||||
|
||||
GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
if (((FindPhone.Response) response).start)
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
|
||||
else
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
|
||||
support.evaluateGBDeviceEvent(findPhoneEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean dndActive() {
|
||||
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(support.getDeviceMac());
|
||||
|
||||
String dndSwitch = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB, "off");
|
||||
if (dndSwitch.equals(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_OFF))
|
||||
return false;
|
||||
|
||||
String startStr = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_START, "00:00");
|
||||
if (dndSwitch.equals("automatic")) startStr = "00:00";
|
||||
String endStr = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_END, "23:59");
|
||||
if (dndSwitch.equals("automatic")) endStr = "23:59";
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
LocalTime currentTime = LocalTime.now();
|
||||
LocalTime start = LocalTime.parse(startStr);
|
||||
LocalTime end = LocalTime.parse(endStr);
|
||||
|
||||
if (start.isAfter(currentTime))
|
||||
return false;
|
||||
if (end.isBefore(currentTime))
|
||||
return false;
|
||||
} else {
|
||||
@SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm");
|
||||
try {
|
||||
Date currentTime = dateFormat.parse(String.format(GBApplication.getLanguage(), "%d:%d",
|
||||
Calendar.getInstance().get(Calendar.HOUR_OF_DAY),
|
||||
Calendar.getInstance().get(Calendar.MINUTE)));
|
||||
Date start = dateFormat.parse(startStr);
|
||||
Date end = dateFormat.parse(endStr);
|
||||
|
||||
assert start != null;
|
||||
if (start.after(currentTime))
|
||||
return false;
|
||||
assert end != null;
|
||||
if (end.before(currentTime))
|
||||
return false;
|
||||
} catch (ParseException e) {
|
||||
LOG.error("Parse exception for DnD", e);
|
||||
}
|
||||
}
|
||||
|
||||
Calendar date = Calendar.getInstance();
|
||||
String preferenceString = dayOfWeekMap.get(date.get(Calendar.DAY_OF_WEEK));
|
||||
|
||||
return sharedPreferences.getBoolean(preferenceString, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles asynchronous music packet, for the following events:
|
||||
* - The app is opened on the band (sends back music info)
|
||||
* - A button is clicked
|
||||
* - Play
|
||||
* - Pause
|
||||
* - Previous
|
||||
* - Next
|
||||
* - The volume is adjusted
|
||||
* @param response Packet to be handled
|
||||
*/
|
||||
private void handleMusicControls(HuaweiPacket response) throws Request.ResponseParseException {
|
||||
if (response.serviceId == MusicControl.id) {
|
||||
AudioManager audioManager = (AudioManager) this.support.getContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
if (response.commandId == MusicControl.MusicStatusResponse.id) {
|
||||
if (!(response instanceof MusicControl.MusicStatusResponse))
|
||||
throw new Request.ResponseTypeMismatchException(response, MusicControl.MusicStatusResponse.class);
|
||||
|
||||
MusicControl.MusicStatusResponse resp = (MusicControl.MusicStatusResponse) response;
|
||||
if (resp.status != -1 && resp.status != 0x000186A0) {
|
||||
LOG.warn("Music information error, will stop here: " + Integer.toHexString(resp.status));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Music information requested, sending acknowledgement and music info.");
|
||||
SetMusicStatusRequest setMusicStatusRequest = new SetMusicStatusRequest(this.support, MusicControl.MusicStatusResponse.id, MusicControl.successValue);
|
||||
try {
|
||||
setMusicStatusRequest.doPerform();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Failed to send music status request", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Failed to send music status request (1)", e);
|
||||
}
|
||||
// Send Music Info
|
||||
this.support.sendSetMusic();
|
||||
} else if (response.commandId == MusicControl.Control.id) {
|
||||
if (!(response instanceof MusicControl.Control.Response))
|
||||
throw new Request.ResponseTypeMismatchException(response, MusicControl.Control.Response.class);
|
||||
|
||||
MusicControl.Control.Response resp = (MusicControl.Control.Response) response;
|
||||
|
||||
if (resp.buttonPresent) {
|
||||
if (resp.button != MusicControl.Control.Response.Button.Unknown) {
|
||||
GBDeviceEventMusicControl musicControl = new GBDeviceEventMusicControl();
|
||||
switch (resp.button) {
|
||||
case Play:
|
||||
LOG.debug("Music - Play button event received");
|
||||
musicControl.event = GBDeviceEventMusicControl.Event.PLAY;
|
||||
break;
|
||||
case Pause:
|
||||
LOG.debug("Music - Pause button event received");
|
||||
musicControl.event = GBDeviceEventMusicControl.Event.PAUSE;
|
||||
break;
|
||||
case Previous:
|
||||
LOG.debug("Music - Previous button event received");
|
||||
musicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS;
|
||||
break;
|
||||
case Next:
|
||||
LOG.debug("Music - Next button event received");
|
||||
musicControl.event = GBDeviceEventMusicControl.Event.NEXT;
|
||||
break;
|
||||
case Volume_up:
|
||||
LOG.debug("Music - Volume up button event received");
|
||||
musicControl.event = GBDeviceEventMusicControl.Event.VOLUMEUP;
|
||||
break;
|
||||
case Volume_down:
|
||||
LOG.debug("Music - Volume down button event received");
|
||||
musicControl.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
this.support.evaluateGBDeviceEvent(musicControl);
|
||||
}
|
||||
}
|
||||
if (resp.volumePresent) {
|
||||
byte volume = resp.volume;
|
||||
if (volume > audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) {
|
||||
LOG.warn("Music - Received volume is too high: 0x"
|
||||
+ Integer.toHexString(volume)
|
||||
+ " > 0x"
|
||||
+ Integer.toHexString(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
|
||||
);
|
||||
// TODO: probably best to send back an error code, though I wouldn't know which
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT > 28) {
|
||||
if (volume < audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)) {
|
||||
LOG.warn("Music - Received volume is too low: 0x"
|
||||
+ Integer.toHexString(volume)
|
||||
+ " < 0x"
|
||||
+ audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
|
||||
);
|
||||
// TODO: probably best to send back an error code, though I wouldn't know which
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG.debug("Music - Setting volume to: 0x" + Integer.toHexString(volume));
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
|
||||
}
|
||||
|
||||
if (resp.buttonPresent || resp.volumePresent) {
|
||||
SetMusicStatusRequest setMusicStatusRequest = new SetMusicStatusRequest(this.support, MusicControl.Control.id, MusicControl.successValue);
|
||||
try {
|
||||
setMusicStatusRequest.doPerform();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Failed to send music status request", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Failed to send music status request (2)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallControls(HuaweiPacket response) throws Request.ResponseParseException {
|
||||
if (response.serviceId == Calls.id && response.commandId == Calls.AnswerCallResponse.id) {
|
||||
if (!(response instanceof Calls.AnswerCallResponse))
|
||||
throw new Request.ResponseTypeMismatchException(response, Calls.AnswerCallResponse.class);
|
||||
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(support.getDevice().getAddress());
|
||||
|
||||
GBDeviceEventCallControl callControlEvent = new GBDeviceEventCallControl();
|
||||
switch (((Calls.AnswerCallResponse) response).action) {
|
||||
case UNKNOWN:
|
||||
LOG.info("Unknown action for call");
|
||||
return;
|
||||
case CALL_ACCEPT:
|
||||
callControlEvent.event = GBDeviceEventCallControl.Event.ACCEPT;
|
||||
LOG.info("Accepted call");
|
||||
|
||||
if (!prefs.getBoolean("enable_call_accept", true)) {
|
||||
LOG.info("Disabled accepting calls, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case CALL_REJECT:
|
||||
callControlEvent.event = GBDeviceEventCallControl.Event.REJECT;
|
||||
LOG.info("Rejected call");
|
||||
|
||||
if (!prefs.getBoolean("enable_call_reject", true)) {
|
||||
LOG.info("Disabled rejecting calls, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
support.evaluateGBDeviceEvent(callControlEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePhoneInfo(HuaweiPacket response) {
|
||||
if (response.serviceId == DeviceConfig.id && response.commandId == DeviceConfig.PhoneInfo.id) {
|
||||
if (!(response instanceof DeviceConfig.PhoneInfo.Response)) {
|
||||
// TODO: exception
|
||||
return;
|
||||
}
|
||||
DeviceConfig.PhoneInfo.Response phoneInfoResp = (DeviceConfig.PhoneInfo.Response) response;
|
||||
GetPhoneInfoRequest getPhoneInfoReq = new GetPhoneInfoRequest(this.support, phoneInfoResp.info);
|
||||
try {
|
||||
getPhoneInfoReq.doPerform();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMenstrualModifyTime(HuaweiPacket response) {
|
||||
if (response.serviceId == Menstrual.id && response.commandId == Menstrual.ModifyTime.id) {
|
||||
if (!(response instanceof Menstrual.ModifyTime.Response)) {
|
||||
// TODO: exception
|
||||
return;
|
||||
}
|
||||
//Menstrual.ModifyTime.Response menstrualModifyTimeResp = (Menstrual.ModifyTime.Response) response;
|
||||
SendMenstrualModifyTimeRequest sendMenstrualModifyTimeReq = new SendMenstrualModifyTimeRequest(this.support);
|
||||
try {
|
||||
sendMenstrualModifyTimeReq.doPerform();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btbr.AbstractBTBRDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
|
||||
|
||||
public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiBRSupport.class);
|
||||
|
||||
private final HuaweiSupportProvider supportProvider;
|
||||
|
||||
public HuaweiBRSupport() {
|
||||
super(LOG);
|
||||
addSupportedService(HuaweiConstants.UUID_SERVICE_HUAWEI_SDP);
|
||||
setBufferSize(1032);
|
||||
supportProvider = new HuaweiSupportProvider(this);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
return supportProvider.initializeDevice(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connectFirstTime() {
|
||||
supportProvider.setNeedsAuth(true);
|
||||
return connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSocketRead(byte[] data) {
|
||||
supportProvider.onSocketRead(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
supportProvider.onSendConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
supportProvider.onFetchRecordedData(dataTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(int flags) {
|
||||
supportProvider.onReset(flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
supportProvider.onNotification(notificationSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
supportProvider.onSetTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends nodomain.freeyourgadget.gadgetbridge.model.Alarm> alarms) {
|
||||
supportProvider.onSetAlarms(alarms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
supportProvider.onSetCallState(callSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
supportProvider.onSetMusicState(stateSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
supportProvider.onSetMusicInfo(musicSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetPhoneVolume(float volume) {
|
||||
supportProvider.onSetPhoneVolume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindPhone(boolean start) {
|
||||
if (!start)
|
||||
supportProvider.onStopFindPhone();
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
|
||||
public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiLESupport.class);
|
||||
|
||||
private final HuaweiSupportProvider supportProvider;
|
||||
|
||||
public HuaweiLESupport() {
|
||||
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(HuaweiConstants.UUID_SERVICE_HUAWEI_SERVICE);
|
||||
supportProvider = new HuaweiSupportProvider(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
return supportProvider.initializeDevice(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connectFirstTime() {
|
||||
supportProvider.setNeedsAuth(true);
|
||||
return connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
supportProvider.onCharacteristicChanged(characteristic);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
supportProvider.onSendConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
supportProvider.onFetchRecordedData(dataTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(int flags) {
|
||||
supportProvider.onReset(flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
supportProvider.onNotification(notificationSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
supportProvider.onSetTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends nodomain.freeyourgadget.gadgetbridge.model.Alarm> alarms) {
|
||||
supportProvider.onSetAlarms(alarms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
supportProvider.onSetCallState(callSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
supportProvider.onSetMusicState(stateSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
supportProvider.onSetMusicInfo(musicSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetPhoneVolume(float volume) {
|
||||
supportProvider.onSetPhoneVolume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindPhone(boolean start) {
|
||||
if (!start)
|
||||
supportProvider.onStopFindPhone();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,496 @@
|
||||
/* Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.ActivityType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
/**
|
||||
* This class parses the Huawei workouts into the table GB uses to show the workouts
|
||||
* It also re-parses the unknown data from the workout tables
|
||||
* It is a separate class so it can easily be used to re-parse the data without database migrations
|
||||
*/
|
||||
public class HuaweiWorkoutGbParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWorkoutGbParser.class);
|
||||
|
||||
// TODO: Might be nicer to propagate the exceptions, so they can be handled upstream
|
||||
|
||||
public static void parseAllWorkouts() {
|
||||
parseUnknownWorkoutData();
|
||||
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
QueryBuilder<HuaweiWorkoutSummarySample> qb = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder();
|
||||
for (HuaweiWorkoutSummarySample summary : qb.listLazy()) {
|
||||
parseWorkout(summary.getWorkoutId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
GB.toast("Exception parsing workouts", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Exception parsing workouts", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the unknown data from the workout data table
|
||||
*/
|
||||
private static void parseUnknownWorkoutData() {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
QueryBuilder<HuaweiWorkoutDataSample> qb = dbHandler.getDaoSession().getHuaweiWorkoutDataSampleDao().queryBuilder().where(
|
||||
HuaweiWorkoutDataSampleDao.Properties.DataErrorHex.notEq("")
|
||||
);
|
||||
for (HuaweiWorkoutDataSample sample : qb.build().listLazy()) {
|
||||
byte[] data = GB.hexStringToByteArray(new String(sample.getDataErrorHex()));
|
||||
Workout.WorkoutData.Response response = new Workout.WorkoutData.Response(data);
|
||||
|
||||
for (Workout.WorkoutData.Response.Data responseData : response.dataList) {
|
||||
byte[] dataErrorHex;
|
||||
if (responseData.unknownData == null)
|
||||
dataErrorHex = null;
|
||||
else
|
||||
dataErrorHex = StringUtils.bytesToHex(responseData.unknownData).getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
HuaweiWorkoutDataSample dataSample = new HuaweiWorkoutDataSample(
|
||||
sample.getWorkoutId(),
|
||||
responseData.timestamp,
|
||||
responseData.heartRate,
|
||||
responseData.speed,
|
||||
responseData.stepRate,
|
||||
responseData.cadence,
|
||||
responseData.stepLength,
|
||||
responseData.groundContactTime,
|
||||
responseData.impact,
|
||||
responseData.swingAngle,
|
||||
responseData.foreFootLanding,
|
||||
responseData.midFootLanding,
|
||||
responseData.backFootLanding,
|
||||
responseData.eversionAngle,
|
||||
responseData.swolf,
|
||||
responseData.strokeRate,
|
||||
dataErrorHex
|
||||
);
|
||||
|
||||
dbHandler.getDaoSession().getHuaweiWorkoutDataSampleDao().insertOrReplace(dataSample);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
GB.toast("Exception parsing unknown workout data", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Exception parsing unknown workout data", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static int huaweiTypeToGbType(byte huaweiType) {
|
||||
int type = huaweiType & 0xFF;
|
||||
switch (type) {
|
||||
case 1:
|
||||
return ActivityKind.TYPE_RUNNING;
|
||||
case 2:
|
||||
case 13:
|
||||
return ActivityKind.TYPE_WALKING;
|
||||
case 6:
|
||||
return ActivityKind.TYPE_SWIMMING;
|
||||
case 7:
|
||||
return ActivityKind.TYPE_INDOOR_CYCLING;
|
||||
case 129:
|
||||
return ActivityKind.TYPE_BADMINTON;
|
||||
case 130:
|
||||
return ActivityKind.TYPE_EXERCISE; // TODO: Tennis
|
||||
case 132:
|
||||
return ActivityKind.TYPE_BASKETBALL;
|
||||
case 133:
|
||||
return ActivityKind.TYPE_EXERCISE; // TODO: Volleyball
|
||||
case 134:
|
||||
return ActivityKind.TYPE_ELLIPTICAL_TRAINER;
|
||||
case 135:
|
||||
return ActivityKind.TYPE_ROWING_MACHINE;
|
||||
case 173:
|
||||
return ActivityKind.TYPE_EXERCISE; // TODO: Laser tag
|
||||
case 177:
|
||||
return ActivityKind.TYPE_EXERCISE; // TODO: stair climbing
|
||||
case 196:
|
||||
return ActivityKind.TYPE_EXERCISE; // TODO: fishing
|
||||
case 216:
|
||||
return ActivityKind.TYPE_EXERCISE; // TODO: motor racing
|
||||
default:
|
||||
return ActivityKind.TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public static void parseWorkout(Long workoutId) {
|
||||
if (workoutId == null)
|
||||
return;
|
||||
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
QueryBuilder<HuaweiWorkoutSummarySample> qbSummary = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where(
|
||||
HuaweiWorkoutSummarySampleDao.Properties.WorkoutId.eq(workoutId)
|
||||
);
|
||||
List<HuaweiWorkoutSummarySample> summarySamples = qbSummary.build().list();
|
||||
if (summarySamples.size() != 1)
|
||||
return;
|
||||
HuaweiWorkoutSummarySample summary = summarySamples.get(0);
|
||||
|
||||
QueryBuilder<HuaweiWorkoutDataSample> qbData = db.getDaoSession().getHuaweiWorkoutDataSampleDao().queryBuilder().where(
|
||||
HuaweiWorkoutDataSampleDao.Properties.WorkoutId.eq(workoutId)
|
||||
);
|
||||
List<HuaweiWorkoutDataSample> dataSamples = qbData.build().list();
|
||||
|
||||
QueryBuilder<HuaweiWorkoutPaceSample> qbPace = db.getDaoSession().getHuaweiWorkoutPaceSampleDao().queryBuilder().where(
|
||||
HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId)
|
||||
);
|
||||
|
||||
long userId = summary.getUserId();
|
||||
long deviceId = summary.getDeviceId();
|
||||
Date start = new Date(summary.getStartTimestamp() * 1000L);
|
||||
Date end = new Date(summary.getEndTimestamp() * 1000L);
|
||||
|
||||
// Avoid duplicates
|
||||
QueryBuilder<BaseActivitySummary> qb = db.getDaoSession().getBaseActivitySummaryDao().queryBuilder().where(
|
||||
BaseActivitySummaryDao.Properties.UserId.eq(userId),
|
||||
BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId),
|
||||
BaseActivitySummaryDao.Properties.StartTime.eq(start),
|
||||
BaseActivitySummaryDao.Properties.EndTime.eq(end)
|
||||
);
|
||||
List<BaseActivitySummary> duplicates = qb.build().list();
|
||||
BaseActivitySummary previous = null;
|
||||
if (!duplicates.isEmpty())
|
||||
previous = duplicates.get(0);
|
||||
|
||||
int type = huaweiTypeToGbType(summary.getType());
|
||||
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
|
||||
// TODO: Use translatable strings
|
||||
|
||||
JSONObject calories = new JSONObject();
|
||||
calories.put("value", summary.getCalories());
|
||||
calories.put("unit", "calories_unit");
|
||||
jsonObject.put("caloriesBurnt", calories);
|
||||
|
||||
JSONObject distance = new JSONObject();
|
||||
distance.put("value", summary.getDistance());
|
||||
distance.put("unit", "meters");
|
||||
jsonObject.put("distanceMeters", distance);
|
||||
|
||||
JSONObject steps = new JSONObject();
|
||||
steps.put("value", summary.getStepCount());
|
||||
steps.put("unit", "steps_unit");
|
||||
jsonObject.put("steps", steps);
|
||||
|
||||
JSONObject time = new JSONObject();
|
||||
time.put("value", summary.getDuration());
|
||||
time.put("unit", "seconds");
|
||||
jsonObject.put("activeSeconds", time);
|
||||
|
||||
JSONObject status = new JSONObject();
|
||||
status.put("value", summary.getStatus() & 0xFF);
|
||||
status.put("unit", "");
|
||||
jsonObject.put("Status", status);
|
||||
|
||||
JSONObject typeJson = new JSONObject();
|
||||
typeJson.put("value", summary.getType() & 0xFF);
|
||||
typeJson.put("unit", "");
|
||||
jsonObject.put("Type", typeJson);
|
||||
|
||||
JSONObject strokesJson = new JSONObject();
|
||||
strokesJson.put("value", summary.getStrokes());
|
||||
strokesJson.put("unit", "");
|
||||
jsonObject.put("Strokes", strokesJson);
|
||||
|
||||
JSONObject avgStrokeRateJson = new JSONObject();
|
||||
avgStrokeRateJson.put("value", summary.getAvgStrokeRate());
|
||||
avgStrokeRateJson.put("unit", "");
|
||||
jsonObject.put("Average reported stroke rate", avgStrokeRateJson);
|
||||
|
||||
JSONObject poolLengthJson = new JSONObject();
|
||||
poolLengthJson.put("value", summary.getPoolLength());
|
||||
poolLengthJson.put("unit", "cm");
|
||||
jsonObject.put("Pool length", poolLengthJson);
|
||||
|
||||
JSONObject lapsJson = new JSONObject();
|
||||
lapsJson.put("value", summary.getLaps());
|
||||
lapsJson.put("unit", "");
|
||||
jsonObject.put("Laps", lapsJson);
|
||||
|
||||
JSONObject avgSwolfJson = new JSONObject();
|
||||
avgSwolfJson.put("value", summary.getAvgSwolf());
|
||||
avgSwolfJson.put("unit", "");
|
||||
jsonObject.put("Average reported swolf", avgSwolfJson);
|
||||
|
||||
boolean unknownData = false;
|
||||
if (dataSamples.size() != 0) {
|
||||
int speed = 0;
|
||||
int stepRate = 0;
|
||||
int cadence = 0;
|
||||
int stepLength = 0;
|
||||
int groundContactTime = 0;
|
||||
int impact = 0;
|
||||
int maxImpact = 0;
|
||||
int swingAngle = 0;
|
||||
int foreFootLanding = 0;
|
||||
int midFootLanding = 0;
|
||||
int backFootLanding = 0;
|
||||
int eversionAngle = 0;
|
||||
int maxEversionAngle = 0;
|
||||
int swolf = 0;
|
||||
int maxSwolf = 0;
|
||||
int strokeRate = 0;
|
||||
int maxStrokeRate = 0;
|
||||
for (HuaweiWorkoutDataSample dataSample : dataSamples) {
|
||||
speed += dataSample.getSpeed();
|
||||
stepRate += dataSample.getStepRate();
|
||||
cadence += dataSample.getCadence();
|
||||
stepLength += dataSample.getStepLength();
|
||||
groundContactTime += dataSample.getGroundContactTime();
|
||||
impact += dataSample.getImpact();
|
||||
if (dataSample.getImpact() > maxImpact)
|
||||
maxImpact = dataSample.getImpact();
|
||||
swingAngle += dataSample.getSwingAngle();
|
||||
foreFootLanding += dataSample.getForeFootLanding();
|
||||
midFootLanding += dataSample.getMidFootLanding();
|
||||
backFootLanding += dataSample.getBackFootLanding();
|
||||
eversionAngle += dataSample.getEversionAngle();
|
||||
if (dataSample.getEversionAngle() > maxEversionAngle)
|
||||
maxEversionAngle = dataSample.getEversionAngle();
|
||||
swolf += dataSample.getSwolf();
|
||||
if (dataSample.getSwolf() > maxSwolf)
|
||||
maxSwolf = dataSample.getSwolf();
|
||||
strokeRate += dataSample.getStrokeRate();
|
||||
if (dataSample.getStrokeRate() > maxStrokeRate)
|
||||
maxStrokeRate = dataSample.getStrokeRate();
|
||||
if (dataSample.getDataErrorHex() != null)
|
||||
unknownData = true;
|
||||
}
|
||||
// Average the things that should probably be averaged
|
||||
speed = speed / dataSamples.size();
|
||||
cadence = cadence / dataSamples.size();
|
||||
int avgStepRate = stepRate / (summary.getDuration() / 60); // steps per minute
|
||||
|
||||
stepLength = stepLength / dataSamples.size();
|
||||
groundContactTime = groundContactTime / dataSamples.size();
|
||||
impact = impact / dataSamples.size();
|
||||
swingAngle = swingAngle / dataSamples.size();
|
||||
eversionAngle = eversionAngle / dataSamples.size();
|
||||
swolf = swolf / dataSamples.size();
|
||||
strokeRate = strokeRate / dataSamples.size();
|
||||
|
||||
JSONObject speedJson = new JSONObject();
|
||||
speedJson.put("value", speed);
|
||||
speedJson.put("unit", "cm/s");
|
||||
jsonObject.put("Reported speed (avg)", speedJson);
|
||||
|
||||
JSONObject stepRateSumJson = new JSONObject();
|
||||
stepRateSumJson.put("value", stepRate);
|
||||
stepRateSumJson.put("unit", "");
|
||||
jsonObject.put("Step rate (sum)", stepRateSumJson);
|
||||
|
||||
JSONObject stepRateAvgJson = new JSONObject();
|
||||
stepRateAvgJson.put("value", avgStepRate);
|
||||
stepRateAvgJson.put("unit", "steps/min");
|
||||
jsonObject.put("Step rate (avg)", stepRateAvgJson);
|
||||
|
||||
JSONObject cadenceJson = new JSONObject();
|
||||
cadenceJson.put("value", cadence);
|
||||
cadenceJson.put("unit", "steps/min");
|
||||
jsonObject.put("Cadence (avg)", cadenceJson);
|
||||
|
||||
JSONObject stepLengthJson = new JSONObject();
|
||||
stepLengthJson.put("value", stepLength);
|
||||
stepLengthJson.put("unit", "cm");
|
||||
jsonObject.put("Step Length (avg)", stepLengthJson);
|
||||
|
||||
JSONObject groundContactTimeJson = new JSONObject();
|
||||
groundContactTimeJson.put("value", groundContactTime);
|
||||
groundContactTimeJson.put("unit", "milliseconds");
|
||||
jsonObject.put("Ground contact time (avg)", groundContactTimeJson);
|
||||
|
||||
JSONObject impactJson = new JSONObject();
|
||||
impactJson.put("value", impact);
|
||||
impactJson.put("unit", "g");
|
||||
jsonObject.put("Impact (avg)", impactJson);
|
||||
|
||||
JSONObject maxImpactJson = new JSONObject();
|
||||
maxImpactJson.put("value", maxImpact);
|
||||
maxImpactJson.put("unit", "g");
|
||||
jsonObject.put("Impact (max)", maxImpactJson);
|
||||
|
||||
JSONObject swingAngleJson = new JSONObject();
|
||||
swingAngleJson.put("value", swingAngle);
|
||||
swingAngleJson.put("unit", "degrees");
|
||||
jsonObject.put("Swing angle (avg)", swingAngleJson);
|
||||
|
||||
JSONObject foreFootLandingJson = new JSONObject();
|
||||
foreFootLandingJson.put("value", foreFootLanding);
|
||||
foreFootLandingJson.put("unit", "");
|
||||
jsonObject.put("Fore foot landings", foreFootLandingJson);
|
||||
|
||||
JSONObject midFootLandingJson = new JSONObject();
|
||||
midFootLandingJson.put("value", midFootLanding);
|
||||
midFootLandingJson.put("unit", "");
|
||||
jsonObject.put("Mid foot landings", midFootLandingJson);
|
||||
|
||||
JSONObject backFootLandingJson = new JSONObject();
|
||||
backFootLandingJson.put("value", backFootLanding);
|
||||
backFootLandingJson.put("unit", "");
|
||||
jsonObject.put("Back foot landings", backFootLandingJson);
|
||||
|
||||
JSONObject eversionAngleJson = new JSONObject();
|
||||
eversionAngleJson.put("value", eversionAngle);
|
||||
eversionAngleJson.put("unit", "degrees");
|
||||
jsonObject.put("Eversion angle (avg)", eversionAngleJson);
|
||||
|
||||
JSONObject maxEversionAngleJson = new JSONObject();
|
||||
maxEversionAngleJson.put("value", maxEversionAngle);
|
||||
maxEversionAngleJson.put("unit", "degrees");
|
||||
jsonObject.put("Eversion angle (max)", maxEversionAngleJson);
|
||||
|
||||
JSONObject swolfJson = new JSONObject();
|
||||
swolfJson.put("value", swolf);
|
||||
swolfJson.put("unit", "");
|
||||
jsonObject.put("Swolf (avg calculated)", swolfJson);
|
||||
|
||||
JSONObject maxSwolfJson = new JSONObject();
|
||||
maxSwolfJson.put("value", maxSwolf);
|
||||
maxSwolfJson.put("unit", "");
|
||||
jsonObject.put("Swolf (max)", maxSwolfJson);
|
||||
|
||||
JSONObject strokeRateJson = new JSONObject();
|
||||
strokeRateJson.put("value", strokeRate);
|
||||
strokeRateJson.put("unit", "");
|
||||
jsonObject.put("Stroke rate (avg calculated)", strokeRateJson);
|
||||
|
||||
JSONObject maxStrokeRateJson = new JSONObject();
|
||||
maxStrokeRateJson.put("value", maxStrokeRate);
|
||||
maxStrokeRateJson.put("unit", "");
|
||||
jsonObject.put("Stroke rate (max)", maxStrokeRateJson);
|
||||
}
|
||||
|
||||
ListIterator<HuaweiWorkoutPaceSample> it = qbPace.build().listIterator();
|
||||
int count = 0;
|
||||
int pace = 0;
|
||||
while (it.hasNext()) {
|
||||
int index = it.nextIndex();
|
||||
HuaweiWorkoutPaceSample sample = it.next();
|
||||
|
||||
count += 1;
|
||||
pace += sample.getPace();
|
||||
|
||||
JSONObject paceDistance = new JSONObject();
|
||||
paceDistance.put("value", sample.getDistance());
|
||||
paceDistance.put("unit", "kilometers");
|
||||
jsonObject.put(String.format(GBApplication.getLanguage() , "Pace %d distance", index), paceDistance);
|
||||
|
||||
JSONObject paceType = new JSONObject();
|
||||
paceType.put("value", sample.getType());
|
||||
paceType.put("unit", ""); // TODO: not sure
|
||||
jsonObject.put(String.format(GBApplication.getLanguage(), "Pace %d type", index), paceType);
|
||||
|
||||
JSONObject pacePace = new JSONObject();
|
||||
pacePace.put("value", sample.getPace());
|
||||
pacePace.put("unit", "seconds_km");
|
||||
jsonObject.put(String.format(GBApplication.getLanguage(), "Pace %d pace", index), pacePace);
|
||||
|
||||
if (sample.getCorrection() != 0) {
|
||||
JSONObject paceCorrection = new JSONObject();
|
||||
paceCorrection.put("value", sample.getCorrection());
|
||||
paceCorrection.put("unit", "m");
|
||||
jsonObject.put(String.format(GBApplication.getLanguage(), "Pace %d correction", index), paceCorrection);
|
||||
}
|
||||
}
|
||||
|
||||
if (count != 0) {
|
||||
JSONObject avgPace = new JSONObject();
|
||||
avgPace.put("value", pace / count);
|
||||
avgPace.put("unit", "seconds_km");
|
||||
jsonObject.put("Average pace", avgPace);
|
||||
}
|
||||
|
||||
if (unknownData) {
|
||||
JSONObject unknownDataJson = new JSONObject();
|
||||
unknownDataJson.put("value", "YES");
|
||||
unknownDataJson.put("unit", "string");
|
||||
|
||||
jsonObject.put("Unknown data encountered", unknownDataJson);
|
||||
}
|
||||
|
||||
BaseActivitySummary baseSummary;
|
||||
if (previous == null) {
|
||||
baseSummary = new BaseActivitySummary(
|
||||
null,
|
||||
"Workout " + summary.getWorkoutNumber(),
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
deviceId,
|
||||
userId,
|
||||
jsonObject.toString(),
|
||||
null
|
||||
);
|
||||
} else {
|
||||
baseSummary = new BaseActivitySummary(
|
||||
previous.getId(),
|
||||
previous.getName(),
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
previous.getBaseLongitude(),
|
||||
previous.getBaseLatitude(),
|
||||
previous.getBaseAltitude(),
|
||||
previous.getGpxTrack(),
|
||||
previous.getRawDetailsPath(),
|
||||
deviceId,
|
||||
userId,
|
||||
jsonObject.toString(),
|
||||
null
|
||||
);
|
||||
}
|
||||
db.getDaoSession().getBaseActivitySummaryDao().insertOrReplace(baseSummary);
|
||||
} catch (Exception e) {
|
||||
GB.toast("Exception parsing workout data", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Exception parsing workout data", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
|
||||
|
||||
/**
|
||||
* Manages all response data.
|
||||
*/
|
||||
public class ResponseManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResponseManager.class);
|
||||
|
||||
private final List<Request> handlers = Collections.synchronizedList(new ArrayList<>());
|
||||
private HuaweiPacket receivedPacket;
|
||||
private final AsynchronousResponse asynchronousResponse;
|
||||
private final HuaweiSupportProvider support;
|
||||
|
||||
public ResponseManager(HuaweiSupportProvider support) {
|
||||
this.asynchronousResponse = new AsynchronousResponse(support);
|
||||
this.support = support;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a request to the response handler list
|
||||
* @param handler The request to handle responses
|
||||
*/
|
||||
public void addHandler(Request handler) {
|
||||
synchronized (handlers) {
|
||||
handlers.add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a request from the response handler list
|
||||
* @param handler The request to remove
|
||||
*/
|
||||
public void removeHandler(Request handler) {
|
||||
synchronized (handlers) {
|
||||
handlers.remove(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the data into a Huawei Packet.
|
||||
* If the packet is complete, it will be handled by the first request that accepts it,
|
||||
* or as an asynchronous request otherwise.
|
||||
*
|
||||
* @param data The received data
|
||||
*/
|
||||
public void handleData(byte[] data) {
|
||||
try {
|
||||
if (receivedPacket == null)
|
||||
receivedPacket = new HuaweiPacket(support.getParamsProvider()).parse(data);
|
||||
else
|
||||
receivedPacket = receivedPacket.parse(data);
|
||||
} catch (HuaweiPacket.ParseException e) {
|
||||
LOG.error("Packet parse exception", e);
|
||||
|
||||
// Clean up so the next message may be parsed correctly
|
||||
this.receivedPacket = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (receivedPacket.complete) {
|
||||
Request handler = null;
|
||||
synchronized (handlers) {
|
||||
for (Request req : handlers) {
|
||||
if (req.handleResponse(receivedPacket)) {
|
||||
handler = req;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (handler == null) {
|
||||
LOG.debug("Service: " + Integer.toHexString(receivedPacket.serviceId & 0xff) + ", command: " + Integer.toHexString(receivedPacket.commandId & 0xff) + ", asynchronous response.");
|
||||
|
||||
// Asynchronous response
|
||||
asynchronousResponse.handleResponse(receivedPacket);
|
||||
} else {
|
||||
LOG.debug("Service: " + Integer.toHexString(receivedPacket.serviceId & 0xff) + ", command: " + Integer.toHexString(receivedPacket.commandId & 0xff) + ", handled by: " + handler.getClass());
|
||||
|
||||
synchronized (handlers) {
|
||||
handlers.remove(handler);
|
||||
}
|
||||
|
||||
handler.handleResponse();
|
||||
}
|
||||
receivedPacket = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms.EventAlarmsRequest;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms.SmartAlarmRequest;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class AlarmsRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AlarmsRequest.class);
|
||||
|
||||
private EventAlarmsRequest eventAlarmsRequest = null;
|
||||
private SmartAlarmRequest smartAlarmRequest = null;
|
||||
|
||||
public AlarmsRequest(HuaweiSupportProvider support, boolean smart) {
|
||||
super(support);
|
||||
this.serviceId = Alarms.id;
|
||||
this.commandId = smart ? SmartAlarmRequest.id : EventAlarmsRequest.id;
|
||||
if (!smart)
|
||||
eventAlarmsRequest = new EventAlarmsRequest(support.getParamsProvider());
|
||||
}
|
||||
|
||||
public void addEventAlarm(Alarm alarm, boolean increasePosition) {
|
||||
if (!alarm.getUnused()) {
|
||||
byte position = (byte) alarm.getPosition();
|
||||
if (increasePosition)
|
||||
position += 1;
|
||||
eventAlarmsRequest.addEventAlarm(new Alarms.EventAlarm(
|
||||
position,
|
||||
alarm.getEnabled(),
|
||||
(byte) alarm.getHour(),
|
||||
(byte) alarm.getMinute(),
|
||||
(byte) alarm.getRepetition(),
|
||||
alarm.getTitle()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public void buildSmartAlarm(Alarm alarm) {
|
||||
this.smartAlarmRequest = new SmartAlarmRequest(
|
||||
paramsProvider,
|
||||
new Alarms.SmartAlarm(
|
||||
alarm.getEnabled() && !alarm.getUnused(),
|
||||
(byte) alarm.getHour(),
|
||||
(byte) alarm.getMinute(),
|
||||
(byte) alarm.getRepetition(),
|
||||
(byte) 5 // TODO: setting for ahead time
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
if (eventAlarmsRequest != null) {
|
||||
return eventAlarmsRequest.serialize();
|
||||
} else if (smartAlarmRequest != null) {
|
||||
return smartAlarmRequest.serialize();
|
||||
} else {
|
||||
throw new RequestCreationException("No alarms set");
|
||||
}
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Alarm");
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien, MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class DebugRequest extends Request {
|
||||
|
||||
public DebugRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = 0;
|
||||
this.commandId = 0;
|
||||
this.addToResponse = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
String debugString = GBApplication
|
||||
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
|
||||
.getString(HuaweiConstants.PREF_HUAWEI_DEBUG, "1,1,false,(1,/),(2,/),(3,/),(4,/)");
|
||||
HuaweiPacket packet = parseDebugString(debugString);
|
||||
try {
|
||||
return packet.serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DebugString := [service_id] "," [command id] "," [encryptflag] ("," [tlv])*
|
||||
service_id := int
|
||||
| "0x" hex
|
||||
command_id := int
|
||||
| "0x" hex
|
||||
encryptflag := "true"
|
||||
| "t"
|
||||
| "false"
|
||||
| "f"
|
||||
tlv := "(" [tag] "," [typevalue] ")"
|
||||
tag := int
|
||||
| "0x" hex
|
||||
typevalue := [type] [value]
|
||||
| [tlv]
|
||||
type := "/" # Empty tag
|
||||
| "B" # Byte (1 byte)
|
||||
| "S" # Short (2 bytes)
|
||||
| "I" # Integer (4 bytes)
|
||||
| "b" # Boolean
|
||||
| "a" # Array of bytes (in hex)
|
||||
| "-" # String
|
||||
value := [any]
|
||||
*/
|
||||
|
||||
public HuaweiPacket parseDebugString(String debugString) throws RequestCreationException {
|
||||
HuaweiPacket packet = new HuaweiPacket(paramsProvider);
|
||||
|
||||
int current = 0;
|
||||
int nextComma = debugString.indexOf(',');
|
||||
|
||||
if (nextComma < 1 || debugString.length() - current < 2)
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
|
||||
if (debugString.charAt(current+1) == 'x')
|
||||
packet.serviceId = Short.valueOf(debugString.substring(current+2, nextComma), 16).byteValue();
|
||||
else
|
||||
packet.serviceId = Short.valueOf(debugString.substring(current, nextComma)).byteValue();
|
||||
|
||||
current = nextComma + 1;
|
||||
nextComma = debugString.indexOf(',', current);
|
||||
|
||||
if (nextComma < 1 || debugString.length() - current < 2)
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
|
||||
if (debugString.charAt(current+1) == 'x')
|
||||
packet.commandId = Short.valueOf(debugString.substring(current+2, nextComma), 16).byteValue();
|
||||
else
|
||||
packet.commandId = Short.valueOf(debugString.substring(current, nextComma)).byteValue();
|
||||
|
||||
current = nextComma + 1;
|
||||
nextComma = debugString.indexOf(',', current);
|
||||
|
||||
if (debugString.length() - current < 2)
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
if (nextComma < 0)
|
||||
nextComma = debugString.length(); // For no TLVs
|
||||
|
||||
switch (debugString.substring(current, nextComma)) {
|
||||
case "true":
|
||||
case "t":
|
||||
packet.setEncryption(true);
|
||||
break;
|
||||
case "false":
|
||||
case "f":
|
||||
packet.setEncryption(false);
|
||||
break;
|
||||
default:
|
||||
throw new RequestCreationException("Boolean is not a boolean");
|
||||
}
|
||||
|
||||
current = nextComma + 1;
|
||||
|
||||
if (current < debugString.length()) {
|
||||
HuaweiTlvParseReturn retv = parseTlv(debugString.substring(current));
|
||||
if (current + retv.parsedCount != debugString.length())
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
packet.setTlv(retv.tlv);
|
||||
}
|
||||
|
||||
packet.complete = true;
|
||||
return packet;
|
||||
}
|
||||
|
||||
private HuaweiTlvParseReturn parseTlv(String tlvString) throws RequestCreationException {
|
||||
HuaweiTLV tlv = new HuaweiTLV();
|
||||
int current = 0;
|
||||
int nextDelim;
|
||||
|
||||
while (current < tlvString.length()) {
|
||||
if (tlvString.charAt(current) != '(')
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
|
||||
current += 1;
|
||||
nextDelim = tlvString.indexOf(',', current);
|
||||
|
||||
if (nextDelim < 1 || tlvString.length() - current < 2)
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
|
||||
byte tag;
|
||||
// Short in between is because Java doesn't like unsigned numbers
|
||||
if (tlvString.charAt(current+1) == 'x')
|
||||
tag = Short.valueOf(tlvString.substring(current+2, nextDelim), 16).byteValue();
|
||||
else
|
||||
tag = Short.valueOf(tlvString.substring(current, nextDelim)).byteValue();
|
||||
|
||||
current = nextDelim + 1;
|
||||
nextDelim = tlvString.indexOf(')', current);
|
||||
|
||||
if (nextDelim < 1)
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
|
||||
if (tlvString.charAt(current) != '(') {
|
||||
char type = tlvString.charAt(current);
|
||||
String value = tlvString.substring(current + 1, nextDelim);
|
||||
|
||||
switch (type) {
|
||||
case '/':
|
||||
tlv.put(tag);
|
||||
break;
|
||||
case 'B':
|
||||
tlv.put(tag, Byte.parseByte(value));
|
||||
break;
|
||||
case 'S':
|
||||
tlv.put(tag, Short.parseShort(value));
|
||||
break;
|
||||
case 'I':
|
||||
tlv.put(tag, Integer.parseInt(value));
|
||||
break;
|
||||
case 'b':
|
||||
tlv.put(tag, value.equals("1"));
|
||||
break;
|
||||
case 'a':
|
||||
tlv.put(tag, GB.hexStringToByteArray(value));
|
||||
break;
|
||||
case '-':
|
||||
tlv.put(tag, value);
|
||||
break;
|
||||
default:
|
||||
throw new RequestCreationException("Invalid tag type");
|
||||
}
|
||||
|
||||
current = nextDelim + 1;
|
||||
} else {
|
||||
HuaweiTlvParseReturn retv = parseTlv(tlvString.substring(current));
|
||||
tlv.put(tag, retv.tlv);
|
||||
current += retv.parsedCount + 1;
|
||||
}
|
||||
|
||||
if (current == tlvString.length())
|
||||
break;
|
||||
if (tlvString.charAt(current) == ')')
|
||||
break;
|
||||
if (tlvString.charAt(current) != ',')
|
||||
throw new RequestCreationException("Invalid debug command");
|
||||
|
||||
current += 1;
|
||||
}
|
||||
|
||||
return new HuaweiTlvParseReturn(tlv, current);
|
||||
}
|
||||
|
||||
private static class HuaweiTlvParseReturn {
|
||||
public HuaweiTLV tlv;
|
||||
public Integer parsedCount;
|
||||
|
||||
HuaweiTlvParseReturn(HuaweiTLV tlv, Integer parsedCount) {
|
||||
this.tlv = tlv;
|
||||
this.parsedCount = parsedCount;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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/>. */
|
||||
|
||||
/* In order to be compatible with all devices, request send all possible commands
|
||||
to all possible services. This implies long packet which is not handled on the device.
|
||||
Thus, this request could be sliced in 3 packets. But this command does not support slicing.
|
||||
Thus, one need to send multiple requests and concat the response.
|
||||
Packets should be 240 bytes max */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetActivityTypeRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetActivityTypeRequest.class);
|
||||
|
||||
public GetActivityTypeRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.ActivityType.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
DeviceConfig.ActivityType.Request activityRequest = new DeviceConfig.ActivityType.Request(paramsProvider);
|
||||
try {
|
||||
return activityRequest.serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Activity Type");
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class GetAuthRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetAuthRequest.class);
|
||||
|
||||
protected final byte[] clientNonce;
|
||||
protected short authVersion;
|
||||
protected boolean isHiChainLite = false;
|
||||
protected byte[] doubleNonce;
|
||||
protected byte[] key = null;
|
||||
|
||||
public GetAuthRequest(HuaweiSupportProvider support,
|
||||
Request linkParamsReq) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.Auth.id;
|
||||
this.clientNonce = HuaweiCrypto.generateNonce();
|
||||
doubleNonce = ByteBuffer.allocate(32)
|
||||
.put(((GetLinkParamsRequest)linkParamsReq).serverNonce)
|
||||
.put(clientNonce)
|
||||
.array();
|
||||
this.authVersion = paramsProvider.getAuthVersion();
|
||||
}
|
||||
|
||||
public GetAuthRequest(HuaweiSupportProvider support,
|
||||
Request linkParamsReq,
|
||||
boolean isHiChainLite) {
|
||||
this(support, linkParamsReq);
|
||||
this.isHiChainLite = isHiChainLite;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
huaweiCrypto = new HuaweiCrypto(authVersion, isHiChainLite);
|
||||
byte[] nonce;
|
||||
|
||||
try {
|
||||
if (isHiChainLite) {
|
||||
nonce = clientNonce;
|
||||
key = paramsProvider.getPinCode();
|
||||
if (authVersion == 0x02)
|
||||
key = paramsProvider.getSecretKey();
|
||||
} else { // normal mode
|
||||
nonce = ByteBuffer.allocate(18)
|
||||
.putShort(authVersion)
|
||||
.put(clientNonce)
|
||||
.array();
|
||||
}
|
||||
byte[] challenge = huaweiCrypto.digestChallenge(key, doubleNonce);
|
||||
if (challenge == null)
|
||||
throw new RequestCreationException("Challenge null");
|
||||
return new DeviceConfig.Auth.Request(paramsProvider, challenge, nonce, isHiChainLite).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RequestCreationException("Digest exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Auth");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.Auth.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.Auth.Response.class);
|
||||
|
||||
try {
|
||||
byte[] expectedAnswer = huaweiCrypto.digestResponse(key, doubleNonce);
|
||||
if (expectedAnswer == null)
|
||||
throw new ResponseParseException("Challenge null");
|
||||
byte[] actualAnswer = ((DeviceConfig.Auth.Response) receivedPacket).challengeResponse;
|
||||
if (!Arrays.equals(expectedAnswer, actualAnswer)) {
|
||||
throw new ResponseParseException("Challenge answer mismatch : "
|
||||
+ StringUtils.bytesToHex(actualAnswer)
|
||||
+ " != "
|
||||
+ StringUtils.bytesToHex(expectedAnswer)
|
||||
);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new ResponseParseException("Challenge response digest exception");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetBatteryLevelRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetBatteryLevelRequest.class);
|
||||
|
||||
public GetBatteryLevelRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.BatteryLevel.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.BatteryLevel.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Battery Level");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.BatteryLevel.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.BatteryLevel.Response.class);
|
||||
|
||||
byte batteryLevel = ((DeviceConfig.BatteryLevel.Response) receivedPacket).level;
|
||||
getDevice().setBatteryLevel(batteryLevel);
|
||||
|
||||
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo.level = (int)batteryLevel & 0xff;
|
||||
this.supportProvider.evaluateGBDeviceEvent(batteryInfo);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetBondParamsRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetBondParamsRequest.class);
|
||||
|
||||
public GetBondParamsRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.BondParams.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.BondParams.Request(
|
||||
paramsProvider,
|
||||
supportProvider.getSerial(),
|
||||
supportProvider.getMacAddress()
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle BondParams");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.BondParams.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.BondParams.Response.class);
|
||||
|
||||
paramsProvider.setEncryptionCounter(((DeviceConfig.BondParams.Response) receivedPacket).encryptionCounter);
|
||||
int status = ((DeviceConfig.BondParams.Response) receivedPacket).status;
|
||||
if (status == 1) {
|
||||
stopChain(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetBondRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetBondRequest.class);
|
||||
|
||||
protected String macAddress;
|
||||
|
||||
public GetBondRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.Bond.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.Bond.Request(
|
||||
paramsProvider,
|
||||
supportProvider.getSerial(),
|
||||
supportProvider.getDeviceMac(),
|
||||
huaweiCrypto
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Bond");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetConnectStatusRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetConnectStatusRequest.class);
|
||||
|
||||
public GetConnectStatusRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.ConnectStatusRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.ConnectStatusRequest(paramsProvider).serialize();
|
||||
} catch (CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Connect Status");
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetDeviceStatusRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetDeviceStatusRequest.class);
|
||||
|
||||
public byte status;
|
||||
private boolean askStatus;
|
||||
|
||||
public GetDeviceStatusRequest(HuaweiSupportProvider support, boolean askStatus) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.DeviceStatus.id;
|
||||
this.askStatus = askStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.DeviceStatus.Request(paramsProvider, askStatus).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Device Status");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.DeviceStatus.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.DeviceStatus.Response.class);
|
||||
|
||||
this.status = ((DeviceConfig.DeviceStatus.Response) receivedPacket).status;
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetDndLiftWristTypeRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetDndLiftWristTypeRequest.class);
|
||||
|
||||
public GetDndLiftWristTypeRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.DndLiftWristType.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.DndLiftWristType.Request(
|
||||
paramsProvider
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle DND Allow Content");
|
||||
if (!(receivedPacket instanceof DeviceConfig.DndLiftWristType.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.DndLiftWristType.Response.class);
|
||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(supportProvider.getDeviceMac());
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
editor.putInt(HuaweiConstants.PREF_HUAWEI_DND_LIFT_WRIST_TYPE,
|
||||
((DeviceConfig.DndLiftWristType.Response) receivedPacket).dndLiftWristType);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/* Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetEventAlarmList extends Request {
|
||||
|
||||
public GetEventAlarmList(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = Alarms.id;
|
||||
this.commandId = Alarms.EventAlarmsList.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Alarms.EventAlarmsList.Request(supportProvider.getParamsProvider()).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof Alarms.EventAlarmsList.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, Alarms.EventAlarmsList.Response.class);
|
||||
|
||||
List<Alarm> alarms = new ArrayList<>();
|
||||
|
||||
// Correct for position of smart alarm
|
||||
// Note that the band uses 1 as the first index for event alarms
|
||||
int positionOffset;
|
||||
if (supportProvider.getCoordinator().getHuaweiCoordinator().supportsSmartAlarm(supportProvider.getDevice()))
|
||||
positionOffset = 0;
|
||||
else
|
||||
positionOffset = -1;
|
||||
|
||||
byte usedBitmap = 0;
|
||||
|
||||
for (Alarms.EventAlarm eventAlarm : ((Alarms.EventAlarmsList.Response) receivedPacket).eventAlarms) {
|
||||
alarms.add(new Alarm(
|
||||
0,
|
||||
0,
|
||||
eventAlarm.index + positionOffset,
|
||||
eventAlarm.status,
|
||||
false,
|
||||
false,
|
||||
eventAlarm.repeat,
|
||||
eventAlarm.startHour,
|
||||
eventAlarm.startMinute,
|
||||
false,
|
||||
eventAlarm.name,
|
||||
""
|
||||
));
|
||||
usedBitmap |= 1 << eventAlarm.index;
|
||||
}
|
||||
|
||||
// Add all unused alarms as unused
|
||||
for (int i = 1; i < 6; i++) {
|
||||
if ((usedBitmap & (1 << i)) == 0) {
|
||||
alarms.add(new Alarm(
|
||||
0,
|
||||
0,
|
||||
i + positionOffset,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
"",
|
||||
""
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
supportProvider.saveAlarms(alarms.toArray(new Alarm[]{}));
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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/>. */
|
||||
|
||||
/* In order to be compatible with all devices, request send all possible commands
|
||||
to all possible services. This implies long packet which is not handled on the device.
|
||||
Thus, this request could be sliced in 3 packets. But this command does not support slicing.
|
||||
Thus, one need to send multiple requests and concat the response.
|
||||
Packets should be 240 bytes max */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetExpandCapabilityRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetExpandCapabilityRequest.class);
|
||||
|
||||
public GetExpandCapabilityRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.ExpandCapability.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
DeviceConfig.ExpandCapability.Request expandRequest = new DeviceConfig.ExpandCapability.Request(paramsProvider);
|
||||
try {
|
||||
return expandRequest.serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Expand Capability");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.ExpandCapability.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.ExpandCapability.Response.class);
|
||||
|
||||
supportProvider.getHuaweiCoordinator().saveExpandCapabilities(((DeviceConfig.ExpandCapability.Response) receivedPacket).expandCapabilities);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetFitnessTotalsRequest extends Request {
|
||||
|
||||
public GetFitnessTotalsRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.FitnessTotals.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new FitnessData.FitnessTotals.Request(
|
||||
paramsProvider
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof FitnessData.FitnessTotals.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, FitnessData.FitnessTotals.Response.class);
|
||||
|
||||
int totalSteps = ((FitnessData.FitnessTotals.Response) receivedPacket).totalSteps;
|
||||
int totalCalories = ((FitnessData.FitnessTotals.Response) receivedPacket).totalCalories;
|
||||
int totalDistance = ((FitnessData.FitnessTotals.Response) receivedPacket).totalDistance;
|
||||
|
||||
supportProvider.addTotalFitnessData(totalSteps, totalCalories, totalDistance);
|
||||
}
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig.HiChain;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class GetHiChainRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetHiChainRequest.class);
|
||||
// Attributs used along all operation
|
||||
private HiChain.Request req = null;
|
||||
private byte operationCode = 0x02;
|
||||
private byte step;
|
||||
private byte[] authIdSelf = null;
|
||||
private byte[] authIdPeer = null;
|
||||
private byte[] randSelf = null;
|
||||
private byte[] randPeer = null;
|
||||
private long requestId = 0x00;
|
||||
private JSONObject json = null;
|
||||
private byte[] sessionKey = null;
|
||||
// Attributs used once
|
||||
private byte[] seed = null;
|
||||
private byte[] challenge = null;
|
||||
private byte[] psk = null;
|
||||
|
||||
|
||||
public GetHiChainRequest(HuaweiSupportProvider support, boolean firstConnection) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = HiChain.id;
|
||||
if (firstConnection) {
|
||||
operationCode = 0x01;
|
||||
}
|
||||
this.step = 0x01;
|
||||
}
|
||||
|
||||
public GetHiChainRequest(Request prevReq) {
|
||||
super(prevReq.supportProvider);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = HiChain.id;
|
||||
GetHiChainRequest hcReq = (GetHiChainRequest)prevReq;
|
||||
this.req = hcReq.req;
|
||||
this.requestId = (Long)hcReq.requestId;
|
||||
this.operationCode = (byte)hcReq.operationCode;
|
||||
this.step = (byte)hcReq.step;
|
||||
this.authIdSelf = hcReq.authIdSelf;
|
||||
this.authIdPeer = hcReq.authIdPeer;
|
||||
this.randSelf = hcReq.randSelf;
|
||||
this.randPeer = hcReq.randPeer;
|
||||
this.psk = hcReq.psk;
|
||||
this.json = hcReq.json;
|
||||
this.sessionKey = hcReq.sessionKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
if (requestId == 0x00) {
|
||||
requestId = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
LOG.debug("Request operationCode: " + operationCode + " - step: " + step);
|
||||
if (req == null) req = new HiChain.Request(
|
||||
operationCode,
|
||||
requestId,
|
||||
supportProvider.getAndroidId(),
|
||||
HuaweiConstants.GROUP_ID
|
||||
);
|
||||
HuaweiPacket packet = null;
|
||||
int messageId = step;
|
||||
try {
|
||||
if (step == 0x01) {
|
||||
seed = new byte[32];
|
||||
new Random().nextBytes(seed);
|
||||
randSelf = new byte[16];
|
||||
new Random().nextBytes(randSelf);
|
||||
HiChain.Request.StepOne stepOne = req.new StepOne(paramsProvider, messageId, randSelf, seed );
|
||||
packet = stepOne;
|
||||
} else if (step == 0x02) {
|
||||
byte[] message = ByteBuffer
|
||||
.allocate(randPeer.length + randSelf.length + authIdSelf.length + authIdPeer.length)
|
||||
.put(randSelf)
|
||||
.put(randPeer)
|
||||
.put(authIdPeer)
|
||||
.put(authIdSelf)
|
||||
.array();
|
||||
byte[] selfToken = CryptoUtils.calcHmacSha256(psk, message);
|
||||
HiChain.Request.StepTwo stepTwo = req.new StepTwo(paramsProvider, messageId, selfToken);
|
||||
packet = stepTwo;
|
||||
} else if (step == 0x03) {
|
||||
byte[] salt = ByteBuffer
|
||||
.allocate( randSelf.length + randPeer.length)
|
||||
.put(randSelf)
|
||||
.put(randPeer)
|
||||
.array();
|
||||
byte[] info = "hichain_iso_session_key".getBytes(StandardCharsets.UTF_8);
|
||||
sessionKey = CryptoUtils.hkdfSha256(psk, salt, info, 32);
|
||||
LOG.debug("sessionKey: " + GB.hexdump(sessionKey));
|
||||
if (operationCode == 0x01) {
|
||||
byte[] nonce = new byte[12];
|
||||
new Random().nextBytes(nonce);
|
||||
challenge = new byte[16];
|
||||
new Random().nextBytes(challenge);
|
||||
byte[] aad = "hichain_iso_exchange".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] encData = CryptoUtils.encryptAES_GCM_NoPad(challenge, sessionKey, nonce, aad); //aesGCMNoPadding encrypt(sessionKey as key, challenge to encrypt, nonce as iv)
|
||||
HiChain.Request.StepThree stepThree = req.new StepThree(paramsProvider, messageId, nonce, encData);
|
||||
packet = stepThree;
|
||||
} else {
|
||||
step += 0x01;
|
||||
}
|
||||
}
|
||||
if (step == 0x04) {
|
||||
LOG.debug("Step " + step);
|
||||
byte[] nonce = new byte[12];
|
||||
new Random().nextBytes(nonce);
|
||||
byte[] input = new byte[]{0x00, 0x00, 0x00, 0x00};
|
||||
byte[] aad = "hichain_iso_result".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] encResult = CryptoUtils.encryptAES_GCM_NoPad(input, sessionKey, nonce, aad);
|
||||
HiChain.Request.StepFour stepFour = req.new StepFour(paramsProvider, messageId, nonce, encResult);
|
||||
packet = stepFour;
|
||||
|
||||
}
|
||||
LOG.debug("JSONObject on creation:" + (new JSONObject(packet.getTlv().getString(1))).getJSONObject("payload").toString());
|
||||
return packet.serialize();
|
||||
} catch (Exception e) {
|
||||
// TODO: Make exception explicit
|
||||
throw new RequestCreationException("HiChain exception", e);
|
||||
}
|
||||
//return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof HiChain.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, HiChain.Response.class);
|
||||
|
||||
// TODO: handle failure codes
|
||||
|
||||
HiChain.Response response = (HiChain.Response)receivedPacket;
|
||||
step = response.step;
|
||||
|
||||
LOG.debug("Response operationCode: " + operationCode + " - step: " + step);
|
||||
try {
|
||||
if (step == 0x04) {
|
||||
if (operationCode == 0x01) {
|
||||
LOG.debug("Finished auth operation, go to bind");
|
||||
GetHiChainRequest nextRequest = new GetHiChainRequest(supportProvider, false);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
} else {
|
||||
LOG.debug("Finished bind operation");
|
||||
byte[] salt = ByteBuffer
|
||||
.allocate( randSelf.length + randPeer.length)
|
||||
.put(randSelf)
|
||||
.put(randPeer)
|
||||
.array();
|
||||
byte[] info = "hichain_return_key".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] key = CryptoUtils.hkdfSha256(sessionKey, salt, info, 32);
|
||||
LOG.debug("Final sessionKey:" + GB.hexdump(key));
|
||||
paramsProvider.setSecretKey(key);
|
||||
}
|
||||
} else {
|
||||
if (step == 0x01) {
|
||||
byte[] key = null;
|
||||
authIdSelf = supportProvider.getAndroidId();
|
||||
authIdPeer = response.step1Data.peerAuthId;
|
||||
randPeer = response.step1Data.isoSalt;
|
||||
byte[] peerToken = response.step1Data.token;
|
||||
// GeneratePsk
|
||||
if (operationCode == 0x01) {
|
||||
String pinCodeHexStr = StringUtils.bytesToHex(paramsProvider.getPinCode());
|
||||
byte[] pinCode = pinCodeHexStr.getBytes(StandardCharsets.UTF_8);
|
||||
key = CryptoUtils.digest(pinCode);
|
||||
} else {
|
||||
key = supportProvider.getSecretKey();
|
||||
}
|
||||
psk = CryptoUtils.calcHmacSha256(key, seed);
|
||||
byte[] message = ByteBuffer
|
||||
.allocate(randPeer.length + randSelf.length + authIdSelf.length + authIdPeer.length)
|
||||
.put(randPeer)
|
||||
.put(randSelf)
|
||||
.put(authIdSelf)
|
||||
.put(authIdPeer)
|
||||
.array();
|
||||
byte[] tokenCheck = CryptoUtils.calcHmacSha256(psk, message);
|
||||
if (!Arrays.equals(peerToken, tokenCheck)) {
|
||||
LOG.debug("tokenCheck: " + GB.hexdump(tokenCheck) + " is different than " + GB.hexdump(peerToken));
|
||||
throw new RequestCreationException("tokenCheck: " + GB.hexdump(tokenCheck) + " is different than " + GB.hexdump(peerToken));
|
||||
} else {
|
||||
LOG.debug("Token check passes");
|
||||
}
|
||||
} else if (step == 0x02) {
|
||||
byte[] returnCodeMac = response.step2Data.returnCodeMac;
|
||||
byte[] returnCodeMacCheck = CryptoUtils.calcHmacSha256(psk, new byte[]{0x00, 0x00, 0x00, 0x00});
|
||||
if (!Arrays.equals(returnCodeMacCheck, returnCodeMac)) {
|
||||
LOG.debug("returnCodeMacCheck: " + GB.hexdump(returnCodeMacCheck) + " is different than " + GB.hexdump(returnCodeMac));
|
||||
throw new RequestCreationException("returnCodeMacCheck: " + GB.hexdump(returnCodeMacCheck) + " is different than " + GB.hexdump(returnCodeMac));
|
||||
} else {
|
||||
LOG.debug("returnCodeMac check passes");
|
||||
}
|
||||
} else if (step == 0x03) {
|
||||
if (operationCode == 0x01) {
|
||||
byte[] nonce = response.step3Data.nonce;
|
||||
byte[] encAuthToken = response.step3Data.encAuthToken;
|
||||
byte[] authToken = CryptoUtils.decryptAES_GCM_NoPad(encAuthToken, sessionKey, nonce, challenge);
|
||||
supportProvider.setSecretKey(authToken);
|
||||
LOG.debug("Set secret key");
|
||||
}
|
||||
}
|
||||
this.step += 0x01;
|
||||
GetHiChainRequest nextRequest = new GetHiChainRequest(this);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO: Specify exceptions
|
||||
throw new ResponseParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig.LinkParams;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
// GetLinkParamsRequest<HuaweiBtLESupport, BtLERequest>
|
||||
public class GetLinkParamsRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetLinkParamsRequest.class);
|
||||
|
||||
public byte[] serverNonce;
|
||||
public byte bondState;
|
||||
|
||||
public GetLinkParamsRequest(
|
||||
HuaweiSupportProvider support,
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder
|
||||
) {
|
||||
super(support, builder);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = LinkParams.id;
|
||||
this.serverNonce = new byte[18];
|
||||
isSelfQueue = false;
|
||||
}
|
||||
|
||||
public GetLinkParamsRequest(
|
||||
HuaweiSupportProvider support,
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder
|
||||
) {
|
||||
super(support, builder);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = LinkParams.id;
|
||||
this.serverNonce = new byte[18];
|
||||
isSelfQueue = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new LinkParams.Request(paramsProvider, supportProvider.getHuaweiType()).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle LinkParams");
|
||||
|
||||
if (!(receivedPacket instanceof LinkParams.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, LinkParams.Response.class);
|
||||
|
||||
supportProvider.setProtocolVersion(((LinkParams.Response) receivedPacket).protocolVersion);
|
||||
paramsProvider.setAuthMode(((LinkParams.Response) receivedPacket).authMode);
|
||||
|
||||
paramsProvider.setSliceSize(((LinkParams.Response) receivedPacket).sliceSize);
|
||||
paramsProvider.setMtu(((LinkParams.Response) receivedPacket).mtu);
|
||||
|
||||
this.serverNonce = ((LinkParams.Response) receivedPacket).serverNonce;
|
||||
paramsProvider.setAuthVersion(((LinkParams.Response) receivedPacket).authVersion);
|
||||
|
||||
this.bondState = ((LinkParams.Response) receivedPacket).bondState;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications.NotificationCapabilities;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetNotificationCapabilitiesRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetNotificationCapabilitiesRequest.class);
|
||||
|
||||
public GetNotificationCapabilitiesRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = NotificationCapabilities.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new NotificationCapabilities.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Get Notification Capabilities");
|
||||
|
||||
if (!(receivedPacket instanceof NotificationCapabilities.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, NotificationCapabilities.Response.class);
|
||||
|
||||
supportProvider.getHuaweiCoordinator().saveNotificationCapabilities(((NotificationCapabilities.Response) receivedPacket).capabilities);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications.NotificationConstraints;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetNotificationConstraintsRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetNotificationConstraintsRequest.class);
|
||||
|
||||
public GetNotificationConstraintsRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = NotificationConstraints.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new NotificationConstraints.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Get Notification Constraint");
|
||||
|
||||
if (!(receivedPacket instanceof NotificationConstraints.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, NotificationConstraints.Response.class);
|
||||
|
||||
supportProvider.getHuaweiCoordinator().saveNotificationConstraints(((NotificationConstraints.Response) receivedPacket).constraints);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetPhoneInfoRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetPhoneInfoRequest.class);
|
||||
|
||||
private byte[] phoneInfo;
|
||||
|
||||
public GetPhoneInfoRequest(HuaweiSupportProvider support, byte[] phoneInfo) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.PhoneInfo.id;
|
||||
this.phoneInfo = phoneInfo;
|
||||
this.addToResponse = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.PhoneInfo.Request(paramsProvider, this.phoneInfo).serialize();
|
||||
} catch (CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Phone Info");
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetPincodeRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetPincodeRequest.class);
|
||||
|
||||
public GetPincodeRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.PinCode.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.PinCode.Request(paramsProvider).serialize();
|
||||
} catch (CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Pincode");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.PinCode.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.PinCode.Response.class);
|
||||
|
||||
paramsProvider.setPinCode(((DeviceConfig.PinCode.Response) receivedPacket).pinCode);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetProductInformationRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetProductInformationRequest.class);
|
||||
|
||||
public GetProductInformationRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.ProductInfo.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.ProductInfo.Request(paramsProvider, supportProvider.getHuaweiType()).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Product Information");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.ProductInfo.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.ProductInfo.Response.class);
|
||||
|
||||
getDevice().setFirmwareVersion(((DeviceConfig.ProductInfo.Response) receivedPacket).softwareVersion);
|
||||
getDevice().setFirmwareVersion2(((DeviceConfig.ProductInfo.Response) receivedPacket).hardwareVersion);
|
||||
getDevice().setModel(((DeviceConfig.ProductInfo.Response) receivedPacket).productModel);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetSecurityNegotiationRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetSecurityNegotiationRequest.class);
|
||||
public int authType = 0x00;
|
||||
|
||||
public GetSecurityNegotiationRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.SecurityNegotiation.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.SecurityNegotiation.Request(
|
||||
paramsProvider,
|
||||
paramsProvider.getAuthMode(),
|
||||
supportProvider.getAndroidId(),
|
||||
Build.MODEL
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Security and Negotiation");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.SecurityNegotiation.Response)) {
|
||||
// TODO: exception
|
||||
return;
|
||||
}
|
||||
|
||||
this.authType = ((DeviceConfig.SecurityNegotiation.Response) receivedPacket).authType;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetSettingRelatedRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetSettingRelatedRequest.class);
|
||||
|
||||
public GetSettingRelatedRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.SettingRelated.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.SettingRelated.Request(paramsProvider).serialize();
|
||||
} catch (CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Setting Related");
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetSleepDataCountRequest extends Request {
|
||||
private final int start;
|
||||
private final int end;
|
||||
|
||||
public GetSleepDataCountRequest(
|
||||
HuaweiSupportProvider support,
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder,
|
||||
int start,
|
||||
int end
|
||||
) {
|
||||
super(support, builder);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.MessageCount.sleepId;
|
||||
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public GetSleepDataCountRequest(
|
||||
HuaweiSupportProvider support,
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder,
|
||||
int start,
|
||||
int end
|
||||
) {
|
||||
super(support, builder);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.MessageCount.sleepId;
|
||||
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new FitnessData.MessageCount.Request(
|
||||
paramsProvider,
|
||||
this.commandId,
|
||||
this.start,
|
||||
this.end
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof FitnessData.MessageCount.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, FitnessData.MessageCount.Response.class);
|
||||
|
||||
short count = ((FitnessData.MessageCount.Response) receivedPacket).count;
|
||||
|
||||
if (count > 0) {
|
||||
GetSleepDataRequest nextRequest = new GetSleepDataRequest(supportProvider, count, (short) 0);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetSleepDataRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetSleepDataRequest.class);
|
||||
|
||||
private final short maxCount;
|
||||
private final short count;
|
||||
|
||||
public GetSleepDataRequest(HuaweiSupportProvider support, short maxCount, short count) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.MessageData.sleepId;
|
||||
|
||||
this.maxCount = maxCount;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new FitnessData.MessageData.Request(paramsProvider, this.commandId, this.count).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
// FitnessData.MessageData.SleepResponse response = FitnessData.MessageData.SleepResponse.fromTlv(receivedPacket.tlv);
|
||||
if (!(receivedPacket instanceof FitnessData.MessageData.SleepResponse))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, FitnessData.MessageData.SleepResponse.class);
|
||||
|
||||
FitnessData.MessageData.SleepResponse response = (FitnessData.MessageData.SleepResponse) receivedPacket;
|
||||
|
||||
short receivedCount = response.number;
|
||||
|
||||
if (receivedCount != this.count) {
|
||||
LOG.warn("Counts do not match");
|
||||
}
|
||||
|
||||
for (FitnessData.MessageData.SleepResponse.SubContainer subContainer : response.containers) {
|
||||
// TODO: it might make more sense to convert the timestamp in the FitnessData class
|
||||
int[] timestampInts = new int[6];
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (subContainer.timestamp[i] >= 0)
|
||||
timestampInts[i] = subContainer.timestamp[i];
|
||||
else
|
||||
timestampInts[i] = subContainer.timestamp[i] & 0xFF;
|
||||
}
|
||||
|
||||
int timestamp =
|
||||
(timestampInts[0] << 24) +
|
||||
(timestampInts[1] << 16) +
|
||||
(timestampInts[2] << 8) +
|
||||
(timestampInts[3]);
|
||||
|
||||
int durationInt =
|
||||
(timestampInts[4] << 8L) +
|
||||
(timestampInts[5]);
|
||||
short duration = (short) (durationInt * 60);
|
||||
|
||||
this.supportProvider.addSleepActivity(timestamp, duration, subContainer.type);
|
||||
}
|
||||
|
||||
if (count + 1 < maxCount) {
|
||||
GetSleepDataRequest nextRequest = new GetSleepDataRequest(supportProvider, this.maxCount, (short) (this.count + 1));
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/* Copyright (C) 2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetSmartAlarmList extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetSmartAlarmList.class);
|
||||
|
||||
public GetSmartAlarmList(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = Alarms.id;
|
||||
this.commandId = Alarms.SmartAlarmList.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Alarms.SmartAlarmList.Request(supportProvider.getParamsProvider()).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof Alarms.SmartAlarmList.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, Alarms.SmartAlarmList.Response.class);
|
||||
|
||||
Alarms.SmartAlarm smartAlarm = ((Alarms.SmartAlarmList.Response) receivedPacket).smartAlarm;
|
||||
|
||||
if (smartAlarm != null) {
|
||||
supportProvider.saveAlarms(new Alarm[] {
|
||||
new Alarm(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
smartAlarm.status,
|
||||
true,
|
||||
false,
|
||||
smartAlarm.repeat,
|
||||
smartAlarm.startHour,
|
||||
smartAlarm.startMinute,
|
||||
false,
|
||||
"Smart alarm",
|
||||
""
|
||||
)
|
||||
});
|
||||
} else {
|
||||
// Set empty smart alarm so index zero is always smart alarm
|
||||
supportProvider.saveAlarms(new Alarm[] {
|
||||
new Alarm(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
"Smart alarm",
|
||||
""
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetStepDataCountRequest extends Request {
|
||||
private int start = 0;
|
||||
private int end = 0;
|
||||
|
||||
public GetStepDataCountRequest(HuaweiSupportProvider support, int start, int end) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.MessageCount.stepId;
|
||||
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new FitnessData.MessageCount.Request(paramsProvider, this.commandId, this.start, this.end).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof FitnessData.MessageCount.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, FitnessData.MessageCount.Response.class);
|
||||
|
||||
short count = ((FitnessData.MessageCount.Response) receivedPacket).count;
|
||||
|
||||
if (count > 0) {
|
||||
GetStepDataRequest nextRequest = new GetStepDataRequest(supportProvider, count, (short) 0);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetStepDataRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetStepDataRequest.class);
|
||||
|
||||
short maxCount;
|
||||
short count;
|
||||
|
||||
public GetStepDataRequest(HuaweiSupportProvider support, short maxCount, short count) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.MessageData.stepId;
|
||||
this.maxCount = maxCount;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new FitnessData.MessageData.Request(paramsProvider, this.commandId, this.count).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof FitnessData.MessageData.StepResponse))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, FitnessData.MessageData.StepResponse.class);
|
||||
|
||||
FitnessData.MessageData.StepResponse response = (FitnessData.MessageData.StepResponse) receivedPacket;
|
||||
|
||||
if (response.number != this.count) {
|
||||
LOG.warn("Counts do not match! Received: " + response.number + ", expected: " + this.count);
|
||||
this.count = response.number; // This stops it from going into a loop
|
||||
}
|
||||
|
||||
for (FitnessData.MessageData.StepResponse.SubContainer subContainer : response.containers) {
|
||||
int dataTimestamp = subContainer.timestamp;
|
||||
|
||||
if (subContainer.parsedData != null) {
|
||||
short steps = (short) subContainer.steps;
|
||||
short calories = (short) subContainer.calories;
|
||||
short distance = (short) subContainer.distance;
|
||||
byte heartrate = (byte) subContainer.heartrate;
|
||||
byte spo = (byte) subContainer.spo;
|
||||
|
||||
if (steps == -1)
|
||||
steps = 0;
|
||||
if (calories == -1)
|
||||
calories = 0;
|
||||
if (distance == -1)
|
||||
distance = 0;
|
||||
|
||||
for (FitnessData.MessageData.StepResponse.SubContainer.TV tv : subContainer.unknownTVs) {
|
||||
LOG.warn("Unknown tag in step data: " + tv);
|
||||
}
|
||||
|
||||
this.supportProvider.addStepData(dataTimestamp, steps, calories, distance, spo, heartrate);
|
||||
} else {
|
||||
LOG.error(subContainer.parsedDataError);
|
||||
}
|
||||
}
|
||||
|
||||
if (count + 1 < maxCount) {
|
||||
GetStepDataRequest nextRequest = new GetStepDataRequest(supportProvider, this.maxCount, (short) (this.count + 1));
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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/>. */
|
||||
|
||||
/* In order to be compatible with all devices, request send all possible commands
|
||||
to all possible services. This implies long packet which is not handled on the device.
|
||||
Thus, this request could be sliced in 3 packets. But this command does not support slicing.
|
||||
Thus, one need to send multiple requests and concat the response.
|
||||
Packets should be 240 bytes max */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetSupportedCommandsRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetSupportedCommandsRequest.class);
|
||||
|
||||
private final Map<Integer, byte[]> commandsPerService;
|
||||
private final List<Byte> activatedServices;
|
||||
|
||||
public GetSupportedCommandsRequest(
|
||||
HuaweiSupportProvider support,
|
||||
List<Byte> activatedServices
|
||||
) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.SupportedCommands.id;
|
||||
this.commandsPerService = DeviceConfig.SupportedCommands.commandsPerService;
|
||||
this.activatedServices = activatedServices;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
DeviceConfig.SupportedCommands.Request commandsRequest = new DeviceConfig.SupportedCommands.Request(paramsProvider);
|
||||
byte nextService = activatedServices.remove(0);
|
||||
boolean fits = commandsRequest.addCommandsForService(nextService, this.commandsPerService.get((int) nextService));
|
||||
while (fits && activatedServices.size() > 0) {
|
||||
nextService = activatedServices.remove(0);
|
||||
fits = commandsRequest.addCommandsForService(nextService, this.commandsPerService.get((int) nextService));
|
||||
}
|
||||
if (!fits)
|
||||
activatedServices.add(0, nextService); // Put the extra back
|
||||
try {
|
||||
return commandsRequest.serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
RequestCallback dynamicServicesReq = new RequestCallback() {
|
||||
@Override
|
||||
public void call() {
|
||||
supportProvider.initializeDynamicServices();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Supported Commands");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.SupportedCommands.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.SupportedCommands.Response.class);
|
||||
|
||||
for (DeviceConfig.SupportedCommands.Response.CommandsList commandsList : ((DeviceConfig.SupportedCommands.Response) receivedPacket).commandsLists) {
|
||||
supportProvider.getHuaweiCoordinator().addCommandsForService(
|
||||
commandsList.service,
|
||||
commandsList.commands
|
||||
);
|
||||
}
|
||||
|
||||
if (activatedServices.size() > 0) {
|
||||
GetSupportedCommandsRequest nextRequest = new GetSupportedCommandsRequest(supportProvider, activatedServices);
|
||||
this.nextRequest(nextRequest);
|
||||
} else {
|
||||
supportProvider.getHuaweiCoordinator().printCommandsPerService();
|
||||
if (supportProvider.getHuaweiCoordinator().supportsExpandCapability()) {
|
||||
GetExpandCapabilityRequest nextRequest = new GetExpandCapabilityRequest(supportProvider);
|
||||
nextRequest.setFinalizeReq(dynamicServicesReq);
|
||||
this.nextRequest(nextRequest);
|
||||
} else {
|
||||
dynamicServicesReq.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetSupportedServicesRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetSupportedServicesRequest.class);
|
||||
|
||||
private final byte[] knownSupportedServices;
|
||||
|
||||
public GetSupportedServicesRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.SupportedServices.id;
|
||||
this.knownSupportedServices = DeviceConfig.SupportedServices.knownSupportedServices;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.SupportedServices.Request(paramsProvider, this.knownSupportedServices).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Supported Services");
|
||||
|
||||
if (!(receivedPacket instanceof DeviceConfig.SupportedServices.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.SupportedServices.Response.class);
|
||||
|
||||
byte[] supportedServices = ((DeviceConfig.SupportedServices.Response) receivedPacket).supportedServices;
|
||||
List<Byte> activatedServices = new ArrayList<>();
|
||||
for (int i = 0; i < supportedServices.length; i++) {
|
||||
if (supportedServices[i] == 1) {
|
||||
activatedServices.add(knownSupportedServices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
GetSupportedCommandsRequest supportedCommandsReq = new GetSupportedCommandsRequest(supportProvider, activatedServices);
|
||||
nextRequest(supportedCommandsReq);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig.WearStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetWearStatusRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetWearStatusRequest.class);
|
||||
|
||||
public GetWearStatusRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = WearStatus.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new WearStatus.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Get Wear Status");
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetWorkoutCountRequest extends Request {
|
||||
private final int start;
|
||||
private final int end;
|
||||
|
||||
public GetWorkoutCountRequest(
|
||||
HuaweiSupportProvider support,
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder,
|
||||
int start,
|
||||
int end
|
||||
) {
|
||||
super(support, builder);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = Workout.WorkoutCount.id;
|
||||
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public GetWorkoutCountRequest(
|
||||
HuaweiSupportProvider support,
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder,
|
||||
int start,
|
||||
int end
|
||||
) {
|
||||
super(support, builder);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = Workout.WorkoutCount.id;
|
||||
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Workout.WorkoutCount.Request(paramsProvider, this.start, this.end).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof Workout.WorkoutCount.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutCount.Response.class);
|
||||
|
||||
Workout.WorkoutCount.Response packet = (Workout.WorkoutCount.Response) receivedPacket;
|
||||
|
||||
if (packet.count != packet.workoutNumbers.size())
|
||||
throw new WorkoutParseException("Packet count and workout numbers size do not match.");
|
||||
|
||||
if (packet.count > 0) {
|
||||
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
|
||||
this.supportProvider,
|
||||
packet.workoutNumbers.remove(0),
|
||||
packet.workoutNumbers
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
|
||||
public class GetWorkoutDataRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutDataRequest.class);
|
||||
|
||||
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
|
||||
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
|
||||
short number;
|
||||
Long databaseId;
|
||||
|
||||
/**
|
||||
* Request to get workout totals
|
||||
* @param support The support
|
||||
* @param workoutNumbers The numbers of the current workout
|
||||
* @param remainder The numbers of the remainder if the workouts to get
|
||||
* @param number The number of this data request
|
||||
*/
|
||||
public GetWorkoutDataRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = Workout.WorkoutData.id;
|
||||
|
||||
this.workoutNumbers = workoutNumbers;
|
||||
this.remainder = remainder;
|
||||
this.number = number;
|
||||
|
||||
this.databaseId = databaseId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Workout.WorkoutData.Request(paramsProvider, workoutNumbers.workoutNumber, this.number).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof Workout.WorkoutData.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutData.Response.class);
|
||||
|
||||
Workout.WorkoutData.Response packet = (Workout.WorkoutData.Response) receivedPacket;
|
||||
|
||||
if (packet.workoutNumber != this.workoutNumbers.workoutNumber)
|
||||
throw new WorkoutParseException("Incorrect workout number!");
|
||||
|
||||
if (packet.dataNumber != this.number)
|
||||
throw new WorkoutParseException("Incorrect data number!");
|
||||
|
||||
LOG.info("Workout {} data {}:", this.workoutNumbers.workoutNumber, this.number);
|
||||
LOG.info("Workout : " + packet.workoutNumber);
|
||||
LOG.info("Data num: " + packet.dataNumber);
|
||||
LOG.info("Header : " + Arrays.toString(packet.rawHeader));
|
||||
LOG.info("Header : " + packet.header);
|
||||
LOG.info("Data : " + Arrays.toString(packet.rawData));
|
||||
LOG.info("Data : " + Arrays.toString(packet.dataList.toArray()));
|
||||
LOG.info("Bitmap : " + packet.innerBitmap);
|
||||
|
||||
this.supportProvider.addWorkoutSampleData(
|
||||
this.databaseId,
|
||||
packet.dataList
|
||||
);
|
||||
|
||||
if (this.workoutNumbers.dataCount > this.number + 1) {
|
||||
GetWorkoutDataRequest nextRequest = new GetWorkoutDataRequest(
|
||||
this.supportProvider,
|
||||
this.workoutNumbers,
|
||||
this.remainder,
|
||||
(short) (this.number + 1),
|
||||
databaseId
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
} else if (this.workoutNumbers.paceCount > 0) {
|
||||
GetWorkoutPaceRequest nextRequest = new GetWorkoutPaceRequest(
|
||||
this.supportProvider,
|
||||
this.workoutNumbers,
|
||||
this.remainder,
|
||||
(short) 0,
|
||||
this.databaseId
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
} else {
|
||||
HuaweiWorkoutGbParser.parseWorkout(this.databaseId);
|
||||
|
||||
if (remainder.size() > 0) {
|
||||
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
|
||||
this.supportProvider,
|
||||
remainder.remove(0),
|
||||
remainder
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
|
||||
public class GetWorkoutPaceRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutPaceRequest.class);
|
||||
|
||||
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
|
||||
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
|
||||
short number;
|
||||
Long databaseId;
|
||||
|
||||
public GetWorkoutPaceRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = Workout.WorkoutPace.id;
|
||||
|
||||
this.workoutNumbers = workoutNumbers;
|
||||
this.remainder = remainder;
|
||||
this.number = number;
|
||||
|
||||
this.databaseId = databaseId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Workout.WorkoutPace.Request(paramsProvider,this.workoutNumbers.workoutNumber, this.number).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof Workout.WorkoutPace.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutPace.Response.class);
|
||||
|
||||
Workout.WorkoutPace.Response packet = (Workout.WorkoutPace.Response) receivedPacket;
|
||||
|
||||
if (packet.workoutNumber != this.workoutNumbers.workoutNumber)
|
||||
throw new WorkoutParseException("Incorrect workout number!");
|
||||
|
||||
if (packet.paceNumber != this.number)
|
||||
throw new WorkoutParseException("Incorrect pace number!");
|
||||
|
||||
LOG.info("Workout {} pace {}:", this.workoutNumbers.workoutNumber, this.number);
|
||||
LOG.info("Workout : " + packet.workoutNumber);
|
||||
LOG.info("Pace : " + packet.paceNumber);
|
||||
LOG.info("Block num: " + packet.blocks.size());
|
||||
LOG.info("Blocks : " + Arrays.toString(packet.blocks.toArray()));
|
||||
|
||||
supportProvider.addWorkoutPaceData(this.databaseId, packet.blocks);
|
||||
|
||||
if (this.workoutNumbers.paceCount > this.number + 1) {
|
||||
GetWorkoutPaceRequest nextRequest = new GetWorkoutPaceRequest(
|
||||
this.supportProvider,
|
||||
this.workoutNumbers,
|
||||
this.remainder,
|
||||
(short) (this.number + 1),
|
||||
this.databaseId
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
} else {
|
||||
HuaweiWorkoutGbParser.parseWorkout(this.databaseId);
|
||||
|
||||
if (remainder.size() > 0) {
|
||||
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
|
||||
this.supportProvider,
|
||||
remainder.remove(0),
|
||||
remainder
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
|
||||
public class GetWorkoutTotalsRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutTotalsRequest.class);
|
||||
|
||||
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
|
||||
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
|
||||
|
||||
/**
|
||||
* Request to get workout totals
|
||||
* @param support The support
|
||||
* @param workoutNumbers The numbers of the current workout
|
||||
* @param remainder The numbers of the remainder of the workouts to get
|
||||
*/
|
||||
public GetWorkoutTotalsRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = Workout.WorkoutTotals.id;
|
||||
|
||||
this.workoutNumbers = workoutNumbers;
|
||||
this.remainder = remainder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Workout.WorkoutTotals.Request(paramsProvider, workoutNumbers.workoutNumber).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (!(receivedPacket instanceof Workout.WorkoutTotals.Response))
|
||||
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutTotals.Response.class);
|
||||
|
||||
Workout.WorkoutTotals.Response packet = (Workout.WorkoutTotals.Response) receivedPacket;
|
||||
|
||||
if (packet.number != this.workoutNumbers.workoutNumber)
|
||||
throw new WorkoutParseException("Incorrect workout number!");
|
||||
|
||||
LOG.info("Workout {} totals:", this.workoutNumbers.workoutNumber);
|
||||
LOG.info("Number : " + packet.number);
|
||||
LOG.info("Status : " + packet.status);
|
||||
LOG.info("Start : " + packet.startTime);
|
||||
LOG.info("End : " + packet.endTime);
|
||||
LOG.info("Calories: " + packet.calories);
|
||||
LOG.info("Distance: " + packet.distance);
|
||||
LOG.info("Steps : " + packet.stepCount);
|
||||
LOG.info("Time : " + packet.totalTime);
|
||||
LOG.info("Duration: " + packet.duration);
|
||||
LOG.info("Type : " + packet.type);
|
||||
|
||||
Long databaseId = this.supportProvider.addWorkoutTotalsData(packet);
|
||||
|
||||
// Create the next request
|
||||
if (this.workoutNumbers.dataCount > 0) {
|
||||
GetWorkoutDataRequest nextRequest = new GetWorkoutDataRequest(
|
||||
this.supportProvider,
|
||||
this.workoutNumbers,
|
||||
this.remainder,
|
||||
(short) 0,
|
||||
databaseId
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
} else if (this.workoutNumbers.paceCount > 0) {
|
||||
GetWorkoutPaceRequest nextRequest = new GetWorkoutPaceRequest(
|
||||
this.supportProvider,
|
||||
this.workoutNumbers,
|
||||
this.remainder,
|
||||
(short) 0,
|
||||
databaseId
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
} else {
|
||||
HuaweiWorkoutGbParser.parseWorkout(databaseId);
|
||||
|
||||
if (remainder.size() > 0) {
|
||||
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
|
||||
this.supportProvider,
|
||||
remainder.remove(0),
|
||||
remainder
|
||||
);
|
||||
nextRequest.setFinalizeReq(this.finalizeReq);
|
||||
this.nextRequest(nextRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,304 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
// Based on nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.Request
|
||||
|
||||
/**
|
||||
* Add capacity to :
|
||||
* - chain requests;
|
||||
* - use data from a past request;
|
||||
* - call a function after last request.
|
||||
*/
|
||||
|
||||
public class Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Request.class);
|
||||
|
||||
public static class RequestCreationException extends Exception {
|
||||
public RequestCreationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RequestCreationException(HuaweiPacket.CryptoException e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public RequestCreationException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResponseParseException extends Exception {
|
||||
public ResponseParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ResponseParseException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public ResponseParseException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResponseTypeMismatchException extends ResponseParseException {
|
||||
public ResponseTypeMismatchException(HuaweiPacket a, Class<?> b) {
|
||||
super("Response type mismatch, packet is of type " + a.getClass() + " but expected " + b);
|
||||
}
|
||||
}
|
||||
|
||||
public static class WorkoutParseException extends ResponseParseException {
|
||||
public WorkoutParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
protected OperationStatus operationStatus = OperationStatus.INITIAL;
|
||||
protected byte serviceId;
|
||||
protected byte commandId;
|
||||
protected HuaweiPacket receivedPacket = null;
|
||||
protected HuaweiSupportProvider supportProvider;
|
||||
protected HuaweiPacket.ParamsProvider paramsProvider;
|
||||
private nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builderBr;
|
||||
private nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builderLe;
|
||||
// Be able to autostart a request after this one
|
||||
protected Request nextRequest = null;
|
||||
protected boolean isSelfQueue = false;
|
||||
// Callback function to start after the request
|
||||
protected RequestCallback finalizeReq = null;
|
||||
// Stop chaining requests and clean support.inProgressRequests from these requests
|
||||
protected boolean stopChain = false;
|
||||
protected static HuaweiCrypto huaweiCrypto = null;
|
||||
protected boolean addToResponse = true;
|
||||
|
||||
public static class RequestCallback {
|
||||
protected HuaweiSupportProvider support = null;
|
||||
public RequestCallback() {}
|
||||
public RequestCallback(HuaweiSupportProvider supportProvider) {
|
||||
support = supportProvider;
|
||||
}
|
||||
public void call() {};
|
||||
public void handleException(ResponseParseException e) {
|
||||
LOG.error("Callback request exception", e);
|
||||
};
|
||||
}
|
||||
|
||||
public Request(HuaweiSupportProvider supportProvider, nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder) {
|
||||
this.supportProvider = supportProvider;
|
||||
this.paramsProvider = supportProvider.getParamsProvider();
|
||||
assert !supportProvider.isBLE();
|
||||
this.builderBr = builder;
|
||||
|
||||
this.isSelfQueue = true;
|
||||
}
|
||||
|
||||
public Request(HuaweiSupportProvider supportProvider, nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) {
|
||||
this.supportProvider = supportProvider;
|
||||
this.paramsProvider = supportProvider.getParamsProvider();
|
||||
assert supportProvider.isBLE();
|
||||
this.builderLe = builder;
|
||||
|
||||
this.isSelfQueue = true;
|
||||
}
|
||||
|
||||
public Request(HuaweiSupportProvider supportProvider) {
|
||||
this.supportProvider = supportProvider;
|
||||
this.paramsProvider = supportProvider.getParamsProvider();
|
||||
|
||||
if (!supportProvider.isBLE())
|
||||
this.builderBr = supportProvider.createBrTransactionBuilder(getName());
|
||||
else
|
||||
this.builderLe = supportProvider.createLeTransactionBuilder(getName());
|
||||
|
||||
this.isSelfQueue = true;
|
||||
}
|
||||
|
||||
public void doPerform() throws IOException {
|
||||
if (this.addToResponse) {
|
||||
supportProvider.addInProgressRequest(this);
|
||||
}
|
||||
try {
|
||||
for (byte[] request : createRequest()) {
|
||||
int mtu = paramsProvider.getMtu();
|
||||
if (request.length >= mtu) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(request);
|
||||
byte[] data;
|
||||
while (buffer.hasRemaining()) {
|
||||
int delta = Math.min(mtu, buffer.remaining());
|
||||
data = new byte[delta];
|
||||
buffer.get(data, 0, delta);
|
||||
builderWrite(data);
|
||||
}
|
||||
} else {
|
||||
builderWrite(request);
|
||||
}
|
||||
}
|
||||
builderWait(paramsProvider.getInterval()); // Need to wait a little to let some requests end correctly i.e. Battery Level on reconnection to not print correctly
|
||||
if (isSelfQueue) {
|
||||
performConnected();
|
||||
}
|
||||
} catch (RequestCreationException e) {
|
||||
// We cannot throw the RequestCreationException, so we throw an IOException
|
||||
throw new IOException("Request could not be created", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void processResponse() throws ResponseParseException {}
|
||||
|
||||
public void handleResponse() {
|
||||
try {
|
||||
this.receivedPacket.parseTlv();
|
||||
} catch (HuaweiPacket.ParseException e) {
|
||||
LOG.error("Parse TLV exception", e);
|
||||
if (finalizeReq != null)
|
||||
finalizeReq.handleException(new ResponseParseException("Parse TLV exception", e));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
processResponse();
|
||||
} catch (ResponseParseException e) {
|
||||
if (finalizeReq != null)
|
||||
finalizeReq.handleException(e);
|
||||
return;
|
||||
}
|
||||
if (nextRequest != null && !stopChain) {
|
||||
try {
|
||||
nextRequest.doPerform();
|
||||
} catch (IOException e) {
|
||||
GB.toast(supportProvider.getContext(), "nextRequest failed", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Next request failed", e);
|
||||
if (finalizeReq != null)
|
||||
finalizeReq.handleException(new ResponseParseException("Next request failed", e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (nextRequest == null || stopChain) {
|
||||
operationStatus = OperationStatus.FINISHED;
|
||||
if (finalizeReq != null) {
|
||||
finalizeReq.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelfQueue() {
|
||||
isSelfQueue = true;
|
||||
}
|
||||
|
||||
public Request nextRequest(Request req) {
|
||||
nextRequest = req;
|
||||
nextRequest.setSelfQueue();
|
||||
return this;
|
||||
}
|
||||
|
||||
public void stopChain(Request req) {
|
||||
req.stopChain();
|
||||
Request next = req.nextRequest;
|
||||
if (next != null) {
|
||||
next.stopChain(next);
|
||||
supportProvider.removeInProgressRequests(next);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopChain() {
|
||||
stopChain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for responses from the device
|
||||
* @param response The response packet
|
||||
* @return True if this request handles this response, false otherwise
|
||||
*/
|
||||
public boolean handleResponse(HuaweiPacket response) {
|
||||
if (response.serviceId == serviceId && response.commandId == commandId) {
|
||||
receivedPacket = response;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return supportProvider.getContext();
|
||||
}
|
||||
|
||||
protected GBDevice getDevice() {
|
||||
return supportProvider.getDevice();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
Class<?> thisClass = getClass();
|
||||
while (thisClass.isAnonymousClass()) thisClass = thisClass.getSuperclass();
|
||||
return thisClass.getSimpleName();
|
||||
}
|
||||
|
||||
public void setFinalizeReq(RequestCallback finalizeReq) {
|
||||
this.finalizeReq = finalizeReq;
|
||||
}
|
||||
|
||||
private void builderWrite(byte[] data) {
|
||||
if (!this.supportProvider.isBLE()) {
|
||||
this.builderBr.write(data);
|
||||
} else {
|
||||
BluetoothGattCharacteristic characteristic = supportProvider
|
||||
.getLeCharacteristic(HuaweiConstants.UUID_CHARACTERISTIC_HUAWEI_WRITE);
|
||||
this.builderLe.write(characteristic, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void builderWait(int millis) {
|
||||
if (!this.supportProvider.isBLE())
|
||||
this.builderBr.wait(millis);
|
||||
else
|
||||
this.builderLe.wait(millis);
|
||||
}
|
||||
|
||||
private void performConnected() throws IOException {
|
||||
LOG.debug("Perform connected");
|
||||
|
||||
if (!this.supportProvider.isBLE()) {
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btbr.Transaction transaction = this.builderBr.getTransaction();
|
||||
this.supportProvider.performConnected(transaction);
|
||||
} else {
|
||||
nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction transaction = this.builderLe.getTransaction();
|
||||
this.supportProvider.performConnected(transaction);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendAccountRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendAccountRequest.class);
|
||||
|
||||
public SendAccountRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = AccountRelated.id;
|
||||
this.commandId = AccountRelated.SendAccountToDevice.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new AccountRelated.SendAccountToDevice.Request(paramsProvider).serialize();
|
||||
} catch (CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
LOG.debug("handle Send Account to Device");
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
|
||||
public class SendDndAddRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendDndAddRequest.class);
|
||||
|
||||
public SendDndAddRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.DndAddRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(supportProvider.getDeviceMac());
|
||||
|
||||
int dndLiftWristType = sharedPrefs.getInt(HuaweiConstants.PREF_HUAWEI_DND_LIFT_WRIST_TYPE, 0x00); //Device allow content - accept activation
|
||||
boolean statusDndLiftWrist = sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_LIFT_WRIST, false); //Activate on wrist lift with DND
|
||||
String dndSwitch = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB, "off");
|
||||
boolean dndEnable = !dndSwitch.equals("off");
|
||||
String startStr = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_START, "00:00");
|
||||
if (dndSwitch.equals("automatic")) startStr = "00:00";
|
||||
byte[] start = HuaweiUtil.timeToByte(startStr);
|
||||
String endStr = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_END, "23:59");
|
||||
if (dndSwitch.equals("automatic")) endStr = "23:59";
|
||||
byte[] end = HuaweiUtil.timeToByte(endStr);
|
||||
int cycle = AlarmUtils.createRepetitionMask(
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_MO, true),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_TU, true),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_WE, true),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_TH, true),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_FR, true),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SA, true),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SU, true)
|
||||
);
|
||||
|
||||
try {
|
||||
return new DeviceConfig.DndAddRequest(
|
||||
paramsProvider,
|
||||
dndEnable,
|
||||
start,
|
||||
end,
|
||||
cycle,
|
||||
statusDndLiftWrist ? dndLiftWristType : 0x00,
|
||||
supportProvider.getHuaweiCoordinator().supportsQueryDndLiftWristDisturbType()
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle DND Add");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendDndDeleteRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendDndDeleteRequest.class);
|
||||
|
||||
public SendDndDeleteRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.DndDeleteRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.DndDeleteRequest(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle DND Delete");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendFactoryResetRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendFactoryResetRequest.class);
|
||||
|
||||
public SendFactoryResetRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.FactoryResetRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.FactoryResetRequest(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Factory Reset");
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData.MotionGoal;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendFitnessGoalRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendFitnessGoalRequest.class);
|
||||
|
||||
public SendFitnessGoalRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = MotionGoal.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
// Hardcoded values till interface for goal
|
||||
int stepGoal = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, ActivityUser.defaultUserStepsGoal);
|
||||
int calorieGoal = 0;
|
||||
short durationGoal = 0;
|
||||
return new MotionGoal.Request(paramsProvider,
|
||||
(byte)0x01,
|
||||
(byte)0x00,
|
||||
stepGoal,
|
||||
calorieGoal,
|
||||
durationGoal
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Send Fitness Goal Request");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual.CapabilityRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendMenstrualCapabilityRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendMenstrualCapabilityRequest.class);
|
||||
|
||||
public SendMenstrualCapabilityRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Menstrual.id;
|
||||
this.commandId = CapabilityRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new CapabilityRequest(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Send Menstrual Capability");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual.ModifyTime;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendMenstrualModifyTimeRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendMenstrualModifyTimeRequest.class);
|
||||
|
||||
public SendMenstrualModifyTimeRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Menstrual.id;
|
||||
this.commandId = ModifyTime.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new ModifyTime.Request(paramsProvider, -1, 0).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Send Menstrual Capability");
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendNotificationRequest extends Request {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendNotificationRequest.class);
|
||||
|
||||
private HuaweiPacket packet;
|
||||
|
||||
public SendNotificationRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = Notifications.NotificationActionRequest.id;
|
||||
}
|
||||
|
||||
public static byte getNotificationType(NotificationType type) {
|
||||
switch (type.getGenericType()) {
|
||||
case "generic_social":
|
||||
case "generic_chat":
|
||||
return Notifications.NotificationType.weChat;
|
||||
case "generic_email":
|
||||
return Notifications.NotificationType.email;
|
||||
case "generic":
|
||||
return Notifications.NotificationType.generic;
|
||||
default:
|
||||
return Notifications.NotificationType.sms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void buildNotificationTLVFromNotificationSpec(NotificationSpec notificationSpec) {
|
||||
String title;
|
||||
if (notificationSpec.title != null)
|
||||
title = notificationSpec.title;
|
||||
else
|
||||
title = notificationSpec.sourceName;
|
||||
|
||||
this.packet = new Notifications.NotificationActionRequest(
|
||||
paramsProvider,
|
||||
supportProvider.getNotificationId(),
|
||||
getNotificationType(notificationSpec.type),
|
||||
Notifications.TextEncoding.standard,
|
||||
title,
|
||||
Notifications.TextEncoding.standard,
|
||||
notificationSpec.sender,
|
||||
Notifications.TextEncoding.standard,
|
||||
notificationSpec.body,
|
||||
notificationSpec.sourceAppId
|
||||
);
|
||||
}
|
||||
|
||||
public void buildNotificationTLVFromCallSpec(CallSpec callSpec) {
|
||||
this.packet = new Notifications.NotificationActionRequest(
|
||||
paramsProvider,
|
||||
supportProvider.getNotificationId(),
|
||||
Notifications.NotificationType.call,
|
||||
Notifications.TextEncoding.standard,
|
||||
callSpec.name,
|
||||
Notifications.TextEncoding.standard,
|
||||
callSpec.name,
|
||||
Notifications.TextEncoding.standard,
|
||||
callSpec.name,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return this.packet.serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Notification");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout.NotifyHeartRate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendNotifyHeartRateCapabilityRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendNotifyHeartRateCapabilityRequest.class);
|
||||
|
||||
public SendNotifyHeartRateCapabilityRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Workout.id;
|
||||
this.commandId = NotifyHeartRate.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new NotifyHeartRate.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Send Workout HeartRate Capability Request");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* Copyright (C) 2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData.NotifyRestHeartRate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendNotifyRestHeartRateCapabilityRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendNotifyRestHeartRateCapabilityRequest.class);
|
||||
|
||||
public SendNotifyRestHeartRateCapabilityRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = NotifyRestHeartRate.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new NotifyRestHeartRate.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Send Workout HeartRate Capability Request");
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/* Copyright (C) 2021-2023 Gaignon Damien
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig.SetUpDeviceStatusRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendSetUpDeviceStatusRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SetUpDeviceStatusRequest.class);
|
||||
|
||||
public SendSetUpDeviceStatusRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = SetUpDeviceStatusRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
int relationShip = 1; // Hardcoded value for now - 1 = mainDevice
|
||||
String deviceName = supportProvider.getDevice().getName();
|
||||
try {
|
||||
return new SetUpDeviceStatusRequest(paramsProvider, relationShip, deviceName).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Set Up Device Status");
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SetActivateOnLiftRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SetActivateOnLiftRequest.class);
|
||||
|
||||
public SetActivateOnLiftRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.ActivateOnLiftRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
boolean activate = GBApplication
|
||||
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
|
||||
.getBoolean(DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED, false);
|
||||
try {
|
||||
return new DeviceConfig.ActivateOnLiftRequest(paramsProvider, activate).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Set Activate On Rotate");
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/* Copyright (C) 2021-2022 Gaignon Damien
|
||||
Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
|
||||
public class SetActivityReminderRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SetActivityReminderRequest.class);
|
||||
|
||||
public SetActivityReminderRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.ActivityReminder.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(supportProvider.getDeviceMac());
|
||||
|
||||
boolean longsitSwitch = sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE, false);
|
||||
String longsitInterval = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD, "60");
|
||||
String longsitStart = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_START, "06:00");
|
||||
String longsitEnd = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_END, "23:00");
|
||||
byte[] start = HuaweiUtil.timeToByte(longsitStart);
|
||||
byte[] end = HuaweiUtil.timeToByte(longsitEnd);
|
||||
int cycle = AlarmUtils.createRepetitionMask(
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_MO, false),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TU, false),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_WE, false),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TH, false),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_FR, false),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SA, false),
|
||||
sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SU, false)
|
||||
);
|
||||
|
||||
try {
|
||||
return new FitnessData.ActivityReminder.Request(
|
||||
paramsProvider,
|
||||
longsitSwitch,
|
||||
(byte) Integer.parseInt(longsitInterval),
|
||||
start,
|
||||
end,
|
||||
(byte) cycle
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() {
|
||||
LOG.debug("handle Set Activity Reminder");
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/* Copyright (C) 2022-2023 MartinJM
|
||||
|
||||
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.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SetAutomaticHeartrateRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SetAutomaticHeartrateRequest.class);
|
||||
|
||||
public SetAutomaticHeartrateRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = FitnessData.EnableAutomaticHeartrate.id;
|
||||
this.addToResponse = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
boolean automaticHeartrateEnabled = GBApplication
|
||||
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
|
||||
.getBoolean(DeviceSettingsPreferenceConst.PREF_HEARTRATE_AUTOMATIC_ENABLE, false);
|
||||
if (automaticHeartrateEnabled)
|
||||
LOG.info("Attempting to enable automatic heartrate");
|
||||
else
|
||||
LOG.info("Attempting to disable automatic heartrate");
|
||||
try {
|
||||
return new FitnessData.EnableAutomaticHeartrate.Request(paramsProvider, automaticHeartrateEnabled).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user