Huawei: Add initial support for Huawei-Honor

This commit is contained in:
Damien 'Psolyca' Gaignon 2024-01-07 23:18:08 +01:00
parent ab894ae433
commit 0c22ecdd51
No known key found for this signature in database
GPG Key ID: 9E9404E5D9E11843
149 changed files with 21842 additions and 2 deletions

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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;
}
}

View File

@ -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];
}
};
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
//}
}

View File

@ -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;
}
}

View File

@ -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;
//}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
* }
*/
}

View File

@ -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;
}
}
}
}

View File

@ -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),

View File

@ -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) {}

View File

@ -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();
}
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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[]{}));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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",
""
)
});
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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