From 0c22ecdd5183db3126e41e27cef174fb3ea0fbcf Mon Sep 17 00:00:00 2001 From: Damien 'Psolyca' Gaignon Date: Sun, 7 Jan 2024 23:18:08 +0100 Subject: [PATCH] Huawei: Add initial support for Huawei-Honor --- .../gadgetbridge/daogen/GBDaoGenerator.java | 103 +- .../DeviceSettingsPreferenceConst.java | 22 + .../DeviceSpecificSettingsFragment.java | 14 + .../devices/huawei/HuaweiBRCoordinator.java | 225 +++ .../devices/huawei/HuaweiConstants.java | 74 + .../devices/huawei/HuaweiCoordinator.java | 429 +++++ .../huawei/HuaweiCoordinatorSupplier.java | 48 + .../devices/huawei/HuaweiCrypto.java | 248 +++ .../devices/huawei/HuaweiLECoordinator.java | 225 +++ .../devices/huawei/HuaweiPacket.java | 665 +++++++ .../devices/huawei/HuaweiSampleProvider.java | 530 +++++ .../huawei/HuaweiSettingsCustomizer.java | 254 +++ .../huawei/HuaweiSpo2SampleProvider.java | 223 +++ .../devices/huawei/HuaweiTLV.java | 400 ++++ .../devices/huawei/HuaweiUtil.java | 60 + .../honorband3/HonorBand3Coordinator.java | 74 + .../honorband4/HonorBand4Coordinator.java | 72 + .../honorband5/HonorBand5Coordinator.java | 89 + .../honorband6/HonorBand6Coordinator.java | 84 + .../honorband7/HonorBand7Coordinator.java | 84 + .../HuaweiBand4ProCoordinator.java | 87 + .../huaweiband6/HuaweiBand6Coordinator.java | 84 + .../huaweiband7/HuaweiBand7Coordinator.java | 84 + .../huaweiband8/HuaweiBand8Coordinator.java | 84 + .../HuaweiBandAw70Coordinator.java | 71 + .../HuaweiTalkBandB6Coordinator.java | 60 + .../HuaweiWatchGTCoordinator.java | 90 + .../HuaweiWatchGT2Coordinator.java | 83 + .../HuaweiWatchGT2eCoordinator.java | 94 + .../HuaweiWatchGT3Coordinator.java | 67 + .../huawei/packets/AccountRelated.java | 48 + .../devices/huawei/packets/Alarms.java | 278 +++ .../devices/huawei/packets/Calls.java | 65 + .../devices/huawei/packets/DeviceConfig.java | 1710 +++++++++++++++++ .../packets/DisconnectNotification.java | 45 + .../devices/huawei/packets/FindPhone.java | 63 + .../devices/huawei/packets/FitnessData.java | 563 ++++++ .../devices/huawei/packets/LocaleConfig.java | 53 + .../devices/huawei/packets/Menstrual.java | 75 + .../devices/huawei/packets/MusicControl.java | 185 ++ .../devices/huawei/packets/Notifications.java | 298 +++ .../devices/huawei/packets/WorkMode.java | 58 + .../devices/huawei/packets/Workout.java | 575 ++++++ .../gadgetbridge/model/DeviceType.java | 30 + .../btbr/AbstractBTBRDeviceSupport.java | 3 + .../devices/huawei/AsynchronousResponse.java | 378 ++++ .../devices/huawei/HuaweiBRSupport.java | 121 ++ .../devices/huawei/HuaweiLESupport.java | 129 ++ .../devices/huawei/HuaweiSupportProvider.java | 1636 ++++++++++++++++ .../devices/huawei/HuaweiWorkoutGbParser.java | 496 +++++ .../devices/huawei/ResponseManager.java | 115 ++ .../huawei/requests/AlarmsRequest.java | 95 + .../devices/huawei/requests/DebugRequest.java | 221 +++ .../requests/GetActivityTypeRequest.java | 58 + .../huawei/requests/GetAuthRequest.java | 115 ++ .../requests/GetBatteryLevelRequest.java | 62 + .../huawei/requests/GetBondParamsRequest.java | 64 + .../huawei/requests/GetBondRequest.java | 58 + .../requests/GetConnectStatusRequest.java | 51 + .../requests/GetDeviceStatusRequest.java | 60 + .../requests/GetDndLiftWristTypeRequest.java | 64 + .../huawei/requests/GetEventAlarmList.java | 102 + .../requests/GetExpandCapabilityRequest.java | 62 + .../requests/GetFitnessTotalsRequest.java | 57 + .../huawei/requests/GetHiChainRequest.java | 250 +++ .../huawei/requests/GetLinkParamsRequest.java | 87 + .../GetNotificationCapabilitiesRequest.java | 56 + .../GetNotificationConstraintsRequest.java | 56 + .../huawei/requests/GetPhoneInfoRequest.java | 55 + .../huawei/requests/GetPincodeRequest.java | 57 + .../GetProductInformationRequest.java | 58 + .../GetSecurityNegotiationRequest.java | 66 + .../requests/GetSettingRelatedRequest.java | 51 + .../requests/GetSleepDataCountRequest.java | 85 + .../huawei/requests/GetSleepDataRequest.java | 98 + .../huawei/requests/GetSmartAlarmList.java | 93 + .../requests/GetStepDataCountRequest.java | 62 + .../huawei/requests/GetStepDataRequest.java | 97 + .../requests/GetSupportedCommandsRequest.java | 106 + .../requests/GetSupportedServicesRequest.java | 69 + .../huawei/requests/GetWearStatusRequest.java | 51 + .../requests/GetWorkoutCountRequest.java | 89 + .../requests/GetWorkoutDataRequest.java | 129 ++ .../requests/GetWorkoutPaceRequest.java | 106 + .../requests/GetWorkoutTotalsRequest.java | 120 ++ .../devices/huawei/requests/Request.java | 304 +++ .../huawei/requests/SendAccountRequest.java | 51 + .../huawei/requests/SendDndAddRequest.java | 88 + .../huawei/requests/SendDndDeleteRequest.java | 51 + .../requests/SendFactoryResetRequest.java | 51 + .../requests/SendFitnessGoalRequest.java | 63 + .../SendMenstrualCapabilityRequest.java | 51 + .../SendMenstrualModifyTimeRequest.java | 51 + .../requests/SendNotificationRequest.java | 110 ++ .../SendNotifyHeartRateCapabilityRequest.java | 51 + ...dNotifyRestHeartRateCapabilityRequest.java | 51 + .../SendSetUpDeviceStatusRequest.java | 53 + .../requests/SetActivateOnLiftRequest.java | 56 + .../requests/SetActivityReminderRequest.java | 82 + .../SetAutomaticHeartrateRequest.java | 55 + .../requests/SetAutomaticSpoRequest.java | 55 + .../huawei/requests/SetDateFormatRequest.java | 79 + .../requests/SetDisconnectNotification.java | 60 + .../requests/SetLanguageSettingRequest.java | 77 + .../SetMediumToStrengthThresholdRequest.java | 60 + .../huawei/requests/SetMusicRequest.java | 104 + .../requests/SetMusicStatusRequest.java | 45 + .../requests/SetNavigateOnRotateRequest.java | 56 + .../requests/SetNotificationRequest.java | 56 + .../huawei/requests/SetTimeRequest.java | 51 + .../huawei/requests/SetTimeZoneIdRequest.java | 51 + .../huawei/requests/SetTruSleepRequest.java | 56 + .../requests/SetWearLocationRequest.java | 57 + .../requests/SetWearMessagePushRequest.java | 56 + .../huawei/requests/SetWorkModeRequest.java | 57 + .../huawei/requests/StopFindPhoneRequest.java | 40 + .../requests/StopNotificationRequest.java | 53 + .../gadgetbridge/util/CryptoUtils.java | 89 + app/src/main/res/values/arrays.xml | 15 + app/src/main/res/values/strings.xml | 62 + ...vicesettings_allow_accept_reject_calls.xml | 15 + ...cesettings_disable_find_phone_with_dnd.xml | 8 + ...settings_donotdisturb_allday_liftwirst.xml | 75 + .../res/xml/devicesettings_force_options.xml | 46 + ...icesettings_heartrate_automatic_enable.xml | 9 + .../main/res/xml/devicesettings_huawei.xml | 241 +++ .../res/xml/devicesettings_huawei_debug.xml | 11 + ...cesettings_huawei_reparse_workout_data.xml | 10 + .../devicesettings_inactivity_sheduled.xml | 73 + .../devicesettings_spo_automatic_enable.xml | 9 + .../main/res/xml/devicesettings_trusleep.xml | 19 + .../main/res/xml/devicesettings_workmode.xml | 13 + .../devices/huawei/TestHuaweiCrypto.java | 77 + .../devices/huawei/TestHuaweiPacket.java | 269 +++ .../devices/huawei/TestHuaweiTLV.java | 532 +++++ .../devices/huawei/TestVarInt.java | 72 + .../devices/huawei/packets/TestAlarms.java | 167 ++ .../devices/huawei/packets/TestCalls.java | 68 + .../huawei/packets/TestDeviceConfig.java | 498 +++++ .../packets/TestDisconnectNotification.java | 104 + .../devices/huawei/packets/TestFindPhone.java | 120 ++ .../huawei/packets/TestFitnessData.java | 502 +++++ .../huawei/packets/TestLocaleConfig.java | 87 + .../huawei/packets/TestMusicControl.java | 338 ++++ .../huawei/packets/TestNotifications.java | 197 ++ .../devices/huawei/packets/TestWorkMode.java | 94 + .../devices/huawei/packets/TestWorkout.java | 437 +++++ .../huawei/TestDebugRequestParser.java | 396 ++++ .../devices/huawei/TestResponseManager.java | 449 +++++ 149 files changed, 21842 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiConstants.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinatorSupplier.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCrypto.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSettingsCustomizer.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiUtil.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband3/HonorBand3Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband4/HonorBand4Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband5/HonorBand5Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband6/HonorBand6Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband7/HonorBand7Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband4pro/HuaweiBand4ProCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband6/HuaweiBand6Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband7/HuaweiBand7Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband8/HuaweiBand8Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweibandaw70/HuaweiBandAw70Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweitalkbandb6/HuaweiTalkBandB6Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt/HuaweiWatchGTCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2/HuaweiWatchGT2Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2e/HuaweiWatchGT2eCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt3/HuaweiWatchGT3Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Alarms.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Calls.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DisconnectNotification.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FindPhone.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FitnessData.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/LocaleConfig.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Menstrual.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/MusicControl.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Notifications.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/WorkMode.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/AlarmsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/DebugRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetActivityTypeRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetAuthRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondParamsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetConnectStatusRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDeviceStatusRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDndLiftWristTypeRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetExpandCapabilityRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFitnessTotalsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetHiChainRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetLinkParamsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationCapabilitiesRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationConstraintsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPhoneInfoRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPincodeRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetProductInformationRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSecurityNegotiationRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataCountRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataCountRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWearStatusRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutCountRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendAccountRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndAddRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndDeleteRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFactoryResetRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFitnessGoalRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualCapabilityRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualModifyTimeRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotificationRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyHeartRateCapabilityRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyRestHeartRateCapabilityRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivateOnLiftRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivityReminderRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticHeartrateRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticSpoRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDateFormatRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDisconnectNotification.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetLanguageSettingRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMediumToStrengthThresholdRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicStatusRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNavigateOnRotateRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNotificationRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeZoneIdRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearLocationRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearMessagePushRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWorkModeRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopFindPhoneRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopNotificationRequest.java create mode 100644 app/src/main/res/xml/devicesettings_allow_accept_reject_calls.xml create mode 100644 app/src/main/res/xml/devicesettings_disable_find_phone_with_dnd.xml create mode 100644 app/src/main/res/xml/devicesettings_donotdisturb_allday_liftwirst.xml create mode 100644 app/src/main/res/xml/devicesettings_force_options.xml create mode 100644 app/src/main/res/xml/devicesettings_heartrate_automatic_enable.xml create mode 100644 app/src/main/res/xml/devicesettings_huawei.xml create mode 100644 app/src/main/res/xml/devicesettings_huawei_debug.xml create mode 100644 app/src/main/res/xml/devicesettings_huawei_reparse_workout_data.xml create mode 100644 app/src/main/res/xml/devicesettings_inactivity_sheduled.xml create mode 100644 app/src/main/res/xml/devicesettings_spo_automatic_enable.xml create mode 100644 app/src/main/res/xml/devicesettings_trusleep.xml create mode 100644 app/src/main/res/xml/devicesettings_workmode.xml create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiCrypto.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiPacket.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTLV.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestVarInt.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestAlarms.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestCalls.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDeviceConfig.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDisconnectNotification.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFindPhone.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFitnessData.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestLocaleConfig.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestMusicControl.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestNotifications.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkMode.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkout.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 266540525..ce2001cd0 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -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; } - } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 8fab653be..58951d2c4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -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"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 8df6db0fc..1ac588f72 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java new file mode 100644 index 000000000..c856aab85 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java @@ -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 . */ +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 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 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 qb2 = session.getHuaweiWorkoutSummarySampleDao().queryBuilder(); + List 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 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 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 getDeviceSupportClass() { + return HuaweiBRSupport.class; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiConstants.java new file mode 100644 index 000000000..13e551327 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiConstants.java @@ -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 . */ +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"; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java new file mode 100644 index 000000000..22ee428be --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java @@ -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 . */ +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 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 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinatorSupplier.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinatorSupplier.java new file mode 100644 index 000000000..3709ed031 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinatorSupplier.java @@ -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 . */ +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(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCrypto.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCrypto.java new file mode 100644 index 000000000..d22047a5a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCrypto.java @@ -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 . */ +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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java new file mode 100644 index 000000000..2c359fe01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java @@ -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 . */ +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 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 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 qb2 = session.getHuaweiWorkoutSummarySampleDao().queryBuilder(); + List 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 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 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 getDeviceSupportClass() { + return HuaweiLESupport.class; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java new file mode 100644 index 000000000..3001c6b6d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java @@ -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 . */ + +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 serializeSliced(byte[] serializedTLV) { + List 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 serializeUnsliced(byte[] serializedTLV) { + List 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 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 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 + + '}'; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java new file mode 100644 index 000000000..d93e71e96 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java @@ -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 . */ +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 { + + /* + * 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 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 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 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 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 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 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 getRawOrderedActivitySamples(int timestampFrom, int timestampTo) { + QueryBuilder 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 samples = qb.build().list(); + for (HuaweiActivitySample sample : samples) { + sample.setProvider(this); + } + detachFromSession(); + return samples; + } + + private List getRawOrderedWorkoutSamplesWithHeartRate(int timestampFrom, int timestampTo) { + Device dbDevice = DBHelper.findDevice(getDevice(), getSession()); + if (dbDevice == null) + return Collections.emptyList(); + + QueryBuilder 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 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 getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) { + // Note that the result of this function has to be sorted by timestamp! + + List rawSamples = getRawOrderedActivitySamples(timestamp_from, timestamp_to); + List workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to); + + List processedSamples = new ArrayList<>(); + + Iterator itRawSamples = rawSamples.iterator(); + Iterator 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 interpolate(List processedSamples) { + List 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 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSettingsCustomizer.java new file mode 100644 index 000000000..1cbff4028 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSettingsCustomizer.java @@ -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 . */ +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 getPreferenceKeysWithSummary() { + return Collections.emptySet(); + } + + + public static final Creator CREATOR= new Creator() { + + @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]; + } + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java new file mode 100644 index 000000000..4451309d5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java @@ -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 . */ +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 { + 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 getAllSamples(long timestampFrom, long timestampTo) { + List activitySamples = huaweiSampleProvider.getAllActivitySamples((int) (timestampFrom / 1000L), (int) (timestampTo / 1000L)); + List 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 activitySamples) { + LOG.error("Huawei Spo2 sample provider addSamples called!"); + } + + @Nullable + @Override + public HuaweiSpo2Sample getLatestSample() { + QueryBuilder 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 samples = qb.build().list(); + if (samples.isEmpty()) + return null; + return activityToSpo2Sample(samples.get(0)); + } + + @Nullable + @Override + public HuaweiSpo2Sample getFirstSample() { + QueryBuilder 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 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 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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java new file mode 100644 index 000000000..8c4cb6c6c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java @@ -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 . */ + +/* 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 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 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 getObjects(int tag) { + List 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiUtil.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiUtil.java new file mode 100644 index 000000000..7cb7c1904 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiUtil.java @@ -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 . */ +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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband3/HonorBand3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband3/HonorBand3Coordinator.java new file mode 100644 index 000000000..b960f3f4e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband3/HonorBand3Coordinator.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband4/HonorBand4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband4/HonorBand4Coordinator.java new file mode 100644 index 000000000..3066cceb3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband4/HonorBand4Coordinator.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband5/HonorBand5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband5/HonorBand5Coordinator.java new file mode 100644 index 000000000..179788cc1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband5/HonorBand5Coordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband6/HonorBand6Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband6/HonorBand6Coordinator.java new file mode 100644 index 000000000..dcb89dcb2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband6/HonorBand6Coordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband7/HonorBand7Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband7/HonorBand7Coordinator.java new file mode 100644 index 000000000..df75023c1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/honorband7/HonorBand7Coordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband4pro/HuaweiBand4ProCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband4pro/HuaweiBand4ProCoordinator.java new file mode 100644 index 000000000..dfb4ddb2e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband4pro/HuaweiBand4ProCoordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband6/HuaweiBand6Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband6/HuaweiBand6Coordinator.java new file mode 100644 index 000000000..6277c775e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband6/HuaweiBand6Coordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband7/HuaweiBand7Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband7/HuaweiBand7Coordinator.java new file mode 100644 index 000000000..770932498 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband7/HuaweiBand7Coordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband8/HuaweiBand8Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband8/HuaweiBand8Coordinator.java new file mode 100644 index 000000000..d1acf5a08 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiband8/HuaweiBand8Coordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweibandaw70/HuaweiBandAw70Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweibandaw70/HuaweiBandAw70Coordinator.java new file mode 100644 index 000000000..9c8d90d61 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweibandaw70/HuaweiBandAw70Coordinator.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweitalkbandb6/HuaweiTalkBandB6Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweitalkbandb6/HuaweiTalkBandB6Coordinator.java new file mode 100644 index 000000000..9f9a65e74 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweitalkbandb6/HuaweiTalkBandB6Coordinator.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt/HuaweiWatchGTCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt/HuaweiWatchGTCoordinator.java new file mode 100644 index 000000000..022b161ff --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt/HuaweiWatchGTCoordinator.java @@ -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 . */ +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 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; + //} +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2/HuaweiWatchGT2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2/HuaweiWatchGT2Coordinator.java new file mode 100644 index 000000000..a0c9f4607 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2/HuaweiWatchGT2Coordinator.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2e/HuaweiWatchGT2eCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2e/HuaweiWatchGT2eCoordinator.java new file mode 100644 index 000000000..594256377 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt2e/HuaweiWatchGT2eCoordinator.java @@ -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 . */ +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 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; + //} +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt3/HuaweiWatchGT3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt3/HuaweiWatchGT3Coordinator.java new file mode 100644 index 000000000..5ce883734 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/huaweiwatchgt3/HuaweiWatchGT3Coordinator.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java new file mode 100644 index 000000000..7462e7801 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java @@ -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 . */ +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); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Alarms.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Alarms.java new file mode 100644 index 000000000..da90532f4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Alarms.java @@ -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 . */ +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 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 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; + } + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Calls.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Calls.java new file mode 100644 index 000000000..d513902cd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Calls.java @@ -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 . */ +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); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java new file mode 100644 index 000000000..11e0b2ab3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java @@ -0,0 +1,1710 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +import org.json.JSONException; +import org.json.JSONObject; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiUtil; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier.HuaweiDeviceType; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV.TLV; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +// TODO: complete responses + +public class DeviceConfig { + public static final byte id = 0x01; + + public static class LinkParams { + public static final byte id = 0x01; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, HuaweiDeviceType deviceType) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.tlv = new HuaweiTLV() + .put(0x01) + .put(0x02) + .put(0x03) + .put(0x04); + if (deviceType == HuaweiDeviceType.AW) { + this.tlv.put(0x06); + } + this.complete = true; + this.isEncrypted = false; + } + } + + public static class Response extends HuaweiPacket { + public byte protocolVersion = 0x00; + public short mtu = 0x0014; + public short sliceSize = 0x00f4; + public byte authVersion = 0x00; + public byte[] serverNonce = new byte[16]; + public byte authMode = 0x00; + public byte authAlgo = 0x00; + public byte bondState = 0x00; + public short interval = 0x0; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.isEncrypted = false; + } + + @Override + public void parseTlv() throws ParseException { + if (this.tlv.contains(0x01)) + this.protocolVersion = this.tlv.getByte(0x01); + + if (this.tlv.contains(0x02)) + this.sliceSize = this.tlv.getShort(0x02); + + if (this.tlv.contains(0x03)) + this.mtu = this.tlv.getShort(0x03); + + if (this.tlv.contains(0x04)) + this.interval = this.tlv.getShort(0x04); + + if (this.tlv.contains(0x05)) { + System.arraycopy(this.tlv.getBytes(0x05), 2, this.serverNonce, 0, 16); + this.authVersion = (byte)this.tlv.getBytes(0x05)[1]; + } else + throw new MissingTagException(0x05); + + if (this.tlv.contains(0x07)) + this.authMode = this.tlv.getByte(0x07); + + if (this.tlv.contains(0x08)) + this.authAlgo = this.tlv.getByte(0x08); + + if (this.tlv.contains(0x09)) + this.bondState = this.tlv.getByte(0x09); + } + } + } + + public static class SupportedServices { + public static final byte id = 0x02; + + // notDeviceCapabilities = 0x1C, 0x1E, 0x1F, 0x28, 0x29, 0x2C, 0x2F, 0x31 + // but services = 0x1E, 0x28, 0x2C, 0x31 + // service 0x21 depends on MiddleWear support + public static final byte[] knownSupportedServices = new byte[] { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1D, 0x20, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2A, 0x2B, 0x2D, 0x2E, + 0x30, 0x32, 0x33, 0x34, 0x35 + }; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, byte[] allSupportedServices) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.tlv = new HuaweiTLV() + .put(0x01, allSupportedServices); + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte[] supportedServices; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + if (this.tlv.contains(0x02)) { + this.supportedServices = this.tlv.getBytes(0x02); + } else { + throw new MissingTagException(0x02); + } + } + } + + public static class OutgoingRequest extends HuaweiPacket { + public byte[] allSupportedServices = null; + + public OutgoingRequest(ParamsProvider paramsProvider) { + super(paramsProvider); + this.complete = false; + } + + @Override + public List serialize() throws CryptoException { + return null; + } + + @Override + public void parseTlv() throws ParseException { + if (this.tlv.contains(0x01)) + this.allSupportedServices = this.tlv.getBytes(0x01); + else + throw new MissingTagException(0x01); + } + } + } + + public static class SupportedCommands { + public static final byte id = 0x03; + + public static final TreeMap commandsPerService = new TreeMap() {{ + put(0x01, new byte[] {0x04, 0x07, 0x08, 0x09, 0x0A, 0x0D, 0x0E, 0x10, 0x11, 0x12, 0x13, 0x14, 0x1B, 0x1A, 0x1D, 0x21, 0x22, 0x23, 0x24, 0x29, 0x2A, 0x2B, 0x32, 0x2E, 0x31, 0x30, 0x35, 0x36, 0x37, 0x2F}); + put(0x02, new byte[] {0x01, 0x04, 0x05, 0x06, 0x07, 0x08}); + put(0x03, new byte[] {0x01, 0x03, 0x04}); + put(0x04, new byte[] {0x01}); + put(0x05, new byte[] {0x01}); + put(0x06, new byte[] {0x01}); + put(0x07, new byte[] {0x01, 0x03, 0x05, 0x07, 0x08, 0x09, 0x0A, 0x0E, 0x10, 0x13, 0x16, 0x15, 0x17, 0x18, 0x1B, 0x1C, 0x1D, 0x1E, 0x21, 0x22, 0x23, 0x24, 0x25, 0x28, 0x29, 0x06, 0x1F}); + put(0x08, new byte[] {0x01, 0x02, 0x03}); + put(0x09, new byte[] {0x01, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}); + put(0x0A, new byte[] {0x01, 0x09, 0x0A}); + put(0x0B, new byte[] {0x01, 0x03}); + put(0x0C, new byte[] {0x01}); + put(0x0D, new byte[] {0x01}); + put(0x0F, new byte[] {0x01, 0x03, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C}); + put(0x10, new byte[] {0x01}); + put(0x11, new byte[] {0x01}); + put(0x12, new byte[] {0x01}); + put(0x13, new byte[] {0x01}); + put(0x14, new byte[] {0x01}); + put(0x15, new byte[] {0x01}); + put(0x16, new byte[] {0x01, 0x03, 0x07}); + put(0x17, new byte[] {0x01, 0x04, 0x06, 0x07, 0x0B, 0x0C, 0x10, 0x12, 0x15, 0x17}); + put(0x18, new byte[] {0x01, 0x02, 0x04, 0x05, 0x06, 0x09}); + put(0x19, new byte[] {0x01, 0x04}); + put(0x1A, new byte[] {0x01, 0x03, 0x07, 0x05, 0x06}); + put(0x1B, new byte[] {0x01, 0x0F, 0x19, 0x1A}); + // No 0x1C + put(0x1D, new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}); + // No 0x1E + // No 0x1F + put(0x20, new byte[] {0x01, 0x02, 0x03, 0x04, 0x09, 0x0A}); + //put(0x21, new byte[] {0x01}); + put(0x22, new byte[] {0x01}); + put(0x23, new byte[] {0x02, 0x0B}); + put(0x24, new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}); + put(0x25, new byte[] {0x02, 0x04, 0x0E}); + put(0x26, new byte[] {0x02, 0x03}); + put(0x27, new byte[] {0x01, 0x0E}); + // No 0x28 + // No 0x29 + put(0x2A, new byte[] {0x01, 0x06}); + put(0x2B, new byte[] {0x12}); + // No 0x2C + put(0x2D, new byte[] {0x01}); + put(0x2E, new byte[] {0x01, 0x02, 0x03}); + // No 0x2F + put(0x30, new byte[] {0x01}); + // No 0x31 + put(0x32, new byte[] {0x01}); + put(0x33, new byte[] {0x01, 0x2}); + put(0x34, new byte[] {0x01}); + put(0x35, new byte[] {0x03, 0x04}); + }}; + + + public static class Request extends HuaweiPacket { + private int maxSize; + + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.tlv = new HuaweiTLV(); + + // slice size is the max size + // But there is a 4 byte header, + // a two byte body header, + // a two byte footer, + // three bytes get added for the 0x81 tag, + // and 21 for the encryption stuff + this.maxSize = this.paramsProvider.getSliceSize() - 32; + // Now it gets aligned to 16 byte blocks + // And it gets a header and two length bytes + // For CBC there is also at least one byte padding + this.maxSize = this.maxSize - (this.maxSize % 16) - 4; + } + + public boolean addCommandsForService(byte service, byte[] commands) { + // tlv length is what we have already + // commands is what we'll add + // together with a 3-byte 0x02 tag and two bytes for length + if (this.tlv.length() + commands.length + 5 > this.maxSize) { //this.paramsProvider.mtu - 5 - 2 - 2 - 2 - 21) { + return false; + } + this.tlv.put(0x02, service).put(0x03, commands); + return true; + } + + @Override + public List serialize() throws CryptoException { + this.tlv = new HuaweiTLV() + .put(0x81, this.tlv); + this.complete = true; + return super.serialize(); + } + } + + public static class Response extends HuaweiPacket { + public static class CommandsList { + public int service; + public byte[] commands; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("CommandsList{service="); + sb.append(Integer.toHexString(service)); + sb.append(", commands=["); + for (byte b : commands) { + sb.append(Integer.toHexString(b)); + if (b != commands[commands.length - 1]) // Elements should be unique + sb.append(", "); + } + sb.append("]}"); + return sb.toString(); + } + } + + public List commandsLists; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.isEncrypted = false; + } + + @Override + public void parseTlv() throws ParseException { + this.commandsLists = new ArrayList<>(); + CommandsList commandsList = null; + HuaweiTLV containerTLV = this.tlv.getObject(0x81); + + if (!containerTLV.contains(0x02)) { + throw new MissingTagException(0x02); + } + if (!containerTLV.contains(0x04)) { + throw new MissingTagException(0x04); + } + + for (HuaweiTLV.TLV tlv : containerTLV.get()) { + if ((int) tlv.getTag() == 0x02) { + commandsList = new CommandsList(); + commandsList.service = ByteBuffer.wrap(tlv.getValue()).get(); + } else if ((int) tlv.getTag() == 0x04) { + if (commandsList == null) + throw new SupportedCommandsListException("Commandslist is not yet set"); + ByteBuffer buffer = ByteBuffer.allocate(tlv.getValue().length); + for (int i = 0; i < tlv.getValue().length; i++) { + if ((int) tlv.getValue()[i] == 1) + buffer.put((byte) (commandsPerService.get(commandsList.service)[i])); + } + commandsList.commands = new byte[buffer.position()]; + ((ByteBuffer) buffer.rewind()).get(commandsList.commands); + this.commandsLists.add(commandsList); + } else + throw new SupportedCommandsListException("Unknown tag encountered"); + } + } + } + + // TODO: LogRequest + } + + public static class DateFormat { + public static final byte id = 0x04; + + public static class Request extends HuaweiPacket { + public Request( + ParamsProvider paramsProvider, + byte dateFormat, + byte timeFormat + ) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.tlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, dateFormat) + .put(0x03, timeFormat) + ); + this.complete = true; + } + } + + public static class OutgoingRequest extends HuaweiPacket { + public byte dateFormat; + public byte timeFormat; + + public OutgoingRequest(ParamsProvider paramsProvider) { + super(paramsProvider); + this.complete = false; + } + + @Override + public void parseTlv() throws ParseException { + if (this.tlv.contains(0x02)) + this.dateFormat = this.tlv.getByte(0x02); + + if (this.tlv.contains(0x03)) + this.timeFormat = this.tlv.getByte(0x03); + } + } + } + + public static class TimeRequest extends HuaweiPacket { + public static final byte id = 0x05; + + public TimeRequest(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + ByteBuffer timeAndZoneId = ByteBuffer.wrap(HuaweiUtil.getTimeAndZoneId()); + this.tlv = new HuaweiTLV() + .put(0x01, timeAndZoneId.getInt(0)) + .put(0x02, timeAndZoneId.getShort(4)); + this.complete = true; + } + + // TODO: implement parsing this request for the log parser support + } + + public static class ProductInfo { + public static final byte id = 0x07; + + public static class Request extends HuaweiPacket { + + public Request(ParamsProvider paramsProvider, HuaweiDeviceType deviceType) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.tlv = new HuaweiTLV(); + if (deviceType == HuaweiDeviceType.AW) { + int[] tags = {0x01, 0x02, 0x03, 0x07, 0x09, 0x0A, 0x0c, 0x11}; + for (int i: tags) { + this.tlv.put(i); + } + } else { + int[] tags = {0x01, 0x02, 0x07, 0x09, 0x0A, 0x11, 0x12, 0x16, 0x1A, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23}; + for (int i : tags) { + this.tlv.put(i); + } + } // else if (AW Compatible) + // this.tlv).put(0x01).put(0x02).put(0x07).put(0x09); + // } + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + // TODO: extend: + // public static final int BTVersion = 0x01; + // public static final int productType = 0x02; + // public static final int phoneNumber = 0x04; + // public static final int macAddress = 0x05; + // public static final int IMEI = 0x06; + // public static final int openSourceVersion = 0x08; + // public static final int serialNumber = 0x09; + // public static final int eMMCId = 0x0B; + // public static final int healthAppSupport = 0x0D; + + public String hardwareVersion; + public String softwareVersion; + public String productModel; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.isEncrypted = false; + } + + @Override + public void parseTlv() throws ParseException { + if (!this.tlv.contains(0x07)) + throw new MissingTagException(0x07); + if (!this.tlv.contains(0x0A)) + throw new MissingTagException(0x0A); + if (this.tlv.contains(0x03)) + this.hardwareVersion = this.tlv.getString(0x03); + this.softwareVersion = this.tlv.getString(0x07); + this.productModel = this.tlv.getString(0x0A).trim(); + } + } + + // TODO: implement parsing this request for the log parser support + } + + public static class Bond { + public static final byte id = 0x0E; + + public static class Request extends HuaweiPacket { + public Request( + ParamsProvider paramsProvider, + byte[] clientSerial, + String mac, + HuaweiCrypto huaweiCrypto + ) throws CryptoException { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + byte[] iv = paramsProvider.getIv(); + + try { + this.tlv = new HuaweiTLV() + .put(0x01) + .put(0x03, (byte) 0x00) + .put(0x05, clientSerial) + .put(0x06, huaweiCrypto.encryptBondingKey(paramsProvider.getSecretKey(), mac, iv)) + .put(0x07, iv); + this.isEncrypted = false; + this.complete = true; + } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException e) { + throw new CryptoException("Bonding key creation exception", e); + } + } + } + + public static class OutgoingRequest extends HuaweiPacket { + public byte[] clientSerial; + public byte[] bondingKey; + public byte[] iv; + + public OutgoingRequest(ParamsProvider paramsProvider) { + super(paramsProvider); + this.complete = false; + } + + @Override + public void parseTlv() throws ParseException { + if (this.tlv.contains(0x05)) + this.clientSerial = this.tlv.getBytes(0x05); + else + throw new MissingTagException(0x05); + + if (this.tlv.contains(0x06)) + this.bondingKey = this.tlv.getBytes(0x06); + else + throw new MissingTagException(0x06); + + if (this.tlv.contains(0x07)) + this.iv = this.tlv.getBytes(0x07); + else + throw new MissingTagException(0x07); + } + } + } + + public static class BondParams { + public static final byte id = 0x0F; + + public static class Request extends HuaweiPacket { + public Request( + ParamsProvider paramsProvider, + byte[] clientSerial, + byte[] mac + ) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01) + .put(0x03, clientSerial) + .put(0x04, (byte) 0x02) + .put(0x05) + .put(0x07, mac) + .put(0x09); + this.isEncrypted = false; + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte status; + public long encryptionCounter; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.isEncrypted = false; + } + + @Override + public void parseTlv() { + this.status = this.tlv.getByte(0x01); + this.encryptionCounter = this.tlv.getInteger(0x09) & 0xFFFFFFFFL; + } + } + // TODO: implement parsing this request for the log parser support + } + + public static class ActivityType { + public static final int id = 0x12; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.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); + this.serviceId = DeviceConfig.id; + this.commandId = id; + } + + @Override + public void parseTlv() throws ParseException { + } + } + + } + + public static class Auth { + public static final byte id = 0x13; + + public static class Request extends HuaweiPacket { + public Request( + ParamsProvider paramsProvider, + byte[] challenge, + byte[] nonce, + boolean isHiChainLite + ) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, challenge) + .put(0x02, nonce); + if (isHiChainLite) + this.tlv.put(0x03, (byte)0x02); // Force type 2 + this.isEncrypted = false; + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte[] challengeResponse; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.isEncrypted = false; + } + + @Override + public void parseTlv() { + this.challengeResponse = this.tlv.getBytes(0x01); + } + } + // TODO: implement parsing this request for the log parser support + } + + public static class BatteryLevel { + public static final byte id = 0x08; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01); + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte level; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + + // This differs per watch, so we handle it ourselves in parseTlv + this.isEncrypted = false; + } + + @Override + public void parseTlv() throws ParseException { + if (this.tlv.contains(0x01)) + this.level = this.tlv.getByte(0x01); + else + throw new MissingTagException(0x01); + } + } + // TODO: implement parsing this request for the log parser support + } + + public static class ActivateOnLiftRequest extends HuaweiPacket { + public static final byte id = 0x09; + + public ActivateOnLiftRequest(ParamsProvider paramsProvider, boolean activate) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, activate); + + this.complete = true; + } + // TODO: implement parsing this request for the log parser support + } + + public static class DndDeleteRequest extends HuaweiPacket { + public static final int id = 0x0B; + + public DndDeleteRequest(HuaweiPacket.ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, (byte) 0x01) + ); + this.complete = true; + } + // TODO: implement parsing this request for the log parser support + } + + public static class DndAddRequest extends HuaweiPacket { + public static final int id = 0x0C; + + public DndAddRequest( + HuaweiPacket.ParamsProvider paramsProvider, + boolean dndEnable, + byte[] start, + byte[] end, + int cycle, + int dndLiftWristType, + boolean allowDndLiftWrist + ) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + HuaweiTLV dndPacket = new HuaweiTLV() + .put(0x02, (byte) 0x01) + .put(0x03, dndEnable) + .put(0x04, (byte) 0x00) + .put(0x05, start) + .put(0x06, end) + .put(0x07, (byte) cycle); + + if (allowDndLiftWrist) { + dndPacket.put(0x08, (short) (dndLiftWristType)); + } + this.tlv = new HuaweiTLV() + .put(0x81, dndPacket); + this.complete = true; + } + // TODO: implement parsing this request for the log parser support + } + + public static class FactoryResetRequest extends HuaweiPacket { + public static final byte id = 0x0D; + + public FactoryResetRequest(HuaweiPacket.ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, (byte) 0x01); + + this.complete = true; + } + // TODO: implement parsing this request for the log parser support + } + + public static class PhoneInfo { + public static final byte id = 0x10; + + public static class Request extends HuaweiPacket { + public Request(HuaweiPacket.ParamsProvider paramsProvider, byte[] phoneInfo) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV(); + for (byte b : phoneInfo) { + switch (b) { + case 0xf: + break; + case 0x11: + this.tlv.put((int)b, "1200107310"); // Force AppVersion to 12.1.7.310 + break; + case 0x15: + this.tlv.put((int)b, ""); // Force buildOSPlatformVersion to "" + break; + case 0x10: // Force EmuiBuildVersion to 0x00 + case 0x13: // Force buildOsEnable to 0x00 + case 0x14: // Force buildOSApiVersion to 0x00 + default: + this.tlv.put((int)b, "00"); + } + } + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte[] info; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + } + + @Override + public void parseTlv() { + info = new byte[this.tlv.length()]; + int i = 0; + for (TLV tlv : this.tlv.get()) { + info[i] = tlv.getTag(); + i += 1; + } + } + } + + // TODO: implement parsing this request for the log parser support + } + + public static class DeviceStatus { + public static final byte id = 0x16; + + public static class Request extends HuaweiPacket { + public Request(HuaweiPacket.ParamsProvider paramsProvider, boolean askStatus) { // status or notify + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV(); + if (askStatus) { + this.tlv.put(0x01); + } else { + this.tlv.put(0x02, (byte)0x00); + } + this.isEncrypted = false; + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte status = -1; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.isEncrypted = false; + } + + @Override + public void parseTlv() { + // AW70 doesn't seem to have this + if (this.tlv.contains(0x01)) + this.status = this.tlv.getByte(0x01); + } + } + + // TODO: implement parsing this request for the log parser support + } + + public static class NavigateOnRotateRequest extends HuaweiPacket { + public static final byte id = 0x1B; + + public NavigateOnRotateRequest(HuaweiPacket.ParamsProvider paramsProvider, boolean navigate) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, navigate); + + this.complete = true; + } + // TODO: implement parsing this request for the log parser support + } + + public static class WearLocationRequest extends HuaweiPacket { + public static final byte id = 0x1A; + + public WearLocationRequest(HuaweiPacket.ParamsProvider paramsProvider, byte location) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, location); + + this.complete = true; + } + // TODO: implement parsing this request for the log parser support + } + + public static class DndLiftWristType { + public static final int id = 0x1D; + + public static class Request extends HuaweiPacket { + public Request(HuaweiPacket.ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01); + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public int dndLiftWristType; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() { + this.dndLiftWristType = (int) this.tlv.getShort(0x01); + } + } + // TODO: implement parsing this request for the log parser support + } + + public static class HiChain { + public static final int id = 0x28; + + public static class Request { + private final int operationCode; + private final long requestId; + private final byte[] selfAuthId; + private final String groupId; + private JSONObject version = null; + private JSONObject payload = null; + private JSONObject value = null; + + public Request (int operationCode, long requestId, byte[] selfAuthId, String groupId) { + this.operationCode = operationCode; + this.requestId = requestId; + this.selfAuthId = selfAuthId; + this.groupId = groupId; + } + + public class BaseStep extends HuaweiPacket { + public BaseStep (HuaweiPacket.ParamsProvider paramsProvider, int messageId) throws SerializeException { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = HiChain.id; + this.isSliced = true; + this.isEncrypted = false; + this.complete = true; + version = new JSONObject(); + payload = new JSONObject(); + value = new JSONObject(); + createJson(messageId); + } + } + + public class StepOne extends BaseStep { + + public StepOne ( + HuaweiPacket.ParamsProvider paramsProvider, + int messageId, + byte[] isoSalt, + byte[] seed + ) throws SerializeException { + super(paramsProvider, messageId); + // createJson(1); //messageId); + try { + payload + .put("isoSalt", StringUtils.bytesToHex(isoSalt)) + .put("peerAuthId", StringUtils.bytesToHex(selfAuthId)) + .put("operationCode", operationCode) + .put("seed", StringUtils.bytesToHex(seed)) + .put("peerUserType", 0x00); + if (operationCode == 0x02) { + payload + .put("pkgName", "com.huawei.devicegroupmanage") + .put("serviceType", groupId) + .put("keyLength", 0x20); + value.put("isDeviceLevel", false); + } + this.tlv = new HuaweiTLV() + .put(0x01, value.toString()) + .put(0x02, (byte)operationCode) + .put(0x03, ByteBuffer.allocate(8).putLong(requestId).array()); + //.put(0x04, 0x00) + //.put(0x05, 0x00); + } catch (JSONException e) { + throw new SerializeException("HiChain Step1 JSON exception", e); + } + } + } + + public class StepTwo extends BaseStep { + public StepTwo ( + HuaweiPacket.ParamsProvider paramsProvider, + int messageId, + byte[] token + ) throws SerializeException { + super(paramsProvider, messageId); + // createJson(2); //messageId); + try { + payload + .put("peerAuthId", StringUtils.bytesToHex(selfAuthId)) + .put("token", StringUtils.bytesToHex(token)); + if (operationCode == 0x02) value.put("isDeviceLevel", false); + this.tlv = new HuaweiTLV() + .put(0x01, value.toString()) + .put(0x02, (byte)operationCode) + .put(0x03, ByteBuffer.allocate(8).putLong(requestId).array()); + } catch (JSONException e) { + throw new SerializeException("HiChain Step 2 JSON exception", e); + } + } + } + + public class StepThree extends BaseStep { + public StepThree ( + HuaweiPacket.ParamsProvider paramsProvider, + int messageId, + byte[] nonce, + byte[] encData + ) throws SerializeException { + super(paramsProvider, messageId); + // createJson(3); + try { + payload + .put("nonce", StringUtils.bytesToHex(nonce)) + .put("encData", StringUtils.bytesToHex(encData)); + this.tlv = new HuaweiTLV() + .put(0x01, value.toString()) + .put(0x02, (byte)operationCode) + .put(0x03, ByteBuffer.allocate(8).putLong(requestId).array()); + } catch (JSONException e) { + throw new SerializeException("HiChain Step 3 JSON exception", e); + } + } + } + + public class StepFour extends BaseStep { + public StepFour ( + HuaweiPacket.ParamsProvider paramsProvider, + int messageId, + byte[] nonce, + byte[] encResult + ) throws SerializeException { + super(paramsProvider, messageId); + // if (operationCode == 0x01) { + // createJson(4); //messageId); + // } else { + // createJson(3); + // } + try { + payload + .put("nonce", StringUtils.bytesToHex(nonce)) //generateRandom + .put("encResult", StringUtils.bytesToHex(encResult)) + .put("operationCode", operationCode); + this.tlv = new HuaweiTLV() + .put(0x01, value.toString()) + .put(0x02, (byte)operationCode) + .put(0x03, ByteBuffer.allocate(8).putLong(requestId).array()); + } catch (JSONException e) { + throw new SerializeException("HiChain Step 4 JSON exception", e); + } + } + } + + private void createJson(int messageId) throws HuaweiPacket.SerializeException { + if (operationCode == 0x02) { + messageId |= 0x10; + } + try { + version + .put("minVersion", "1.0.0") + .put("currentVersion", "2.0.16"); + payload + .put("version", version); + value + .put("authForm", 0x00) + .put("payload", payload) + .put("groupAndModuleVersion", "2.0.1") + .put("message", messageId); + if (operationCode == 0x01) { + value + .put("requestId", Long.toString(requestId)) + .put("groupId", groupId) + .put("groupName", "health_group_name") + .put("groupOp", 2) + .put("groupType", 256) + .put("peerDeviceId", new String(selfAuthId, StandardCharsets.UTF_8)) + .put("connDeviceId", new String(selfAuthId, StandardCharsets.UTF_8)) + .put("appId", "com.huawei.health") + .put("ownerName", ""); + } + } catch (JSONException e) { + throw new HuaweiPacket.SerializeException("Create json Exception", e); + } + } + } + + public static class Response extends HuaweiPacket { + // TODO: get rid of GB import... + // TODO: add operation code + + public static class Step1Data { + public byte[] isoSalt; + public byte[] peerAuthId; + public int peerUserType; + public byte[] token; + + public Step1Data(JSONObject payload) throws JSONException { + this.isoSalt = GB.hexStringToByteArray(payload.getString("isoSalt")); + this.peerAuthId = GB.hexStringToByteArray(payload.getString("peerAuthId")); + this.peerUserType = payload.getInt("peerUserType"); + this.token = GB.hexStringToByteArray(payload.getString("token")); + } + + @Override + public String toString() { + return "Step1Data{" + + "isoSalt=" + StringUtils.bytesToHex(isoSalt) + + ", peerAuthId=" + StringUtils.bytesToHex(peerAuthId) + + ", peerUserType=" + peerUserType + + ", token=" + StringUtils.bytesToHex(token) + + '}'; + } + } + + public static class Step2Data { + public byte[] returnCodeMac; + + public Step2Data(JSONObject payload) throws JSONException { + this.returnCodeMac = GB.hexStringToByteArray(payload.getString("returnCodeMac")); + } + + @Override + public String toString() { + return "Step2Data{" + + "returnCodeMac=" + StringUtils.bytesToHex(returnCodeMac) + + '}'; + } + } + + public static class Step3Data { + public byte[] nonce; + public byte[] encAuthToken; + + public Step3Data(JSONObject payload) throws JSONException { + this.nonce = GB.hexStringToByteArray(payload.getString("nonce")); + this.encAuthToken = GB.hexStringToByteArray(payload.getString("encAuthToken")); + } + + @Override + public String toString() { + return "Step3Data{" + + "nonce=" + StringUtils.bytesToHex(nonce) + + ", encAuthToken=" + StringUtils.bytesToHex(encAuthToken) + + '}'; + } + } + + public static class Step4Data { + public String data; + + public Step4Data(HuaweiTLV tlv) { + if (tlv.contains(0x01)) + this.data = tlv.getString(0x01); + } + + @Override + public String toString() { + return "Step4Data{" + + "data='" + data + '\'' + + '}'; + } + } + + // TODO: enum? + // 0 is json? + // 2 is raw string? + public byte type; + + public JSONObject value; + public JSONObject payload; + public JSONObject version; + + public byte step; + // public int operationCode; // TODO + + public Step1Data step1Data; + public Step2Data step2Data; + public Step3Data step3Data; + public Step4Data step4Data; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = HiChain.id; + this.isEncrypted = false; + } + + @Override + public void parseTlv() throws ParseException { + if (!this.tlv.contains(0x01)) + throw new MissingTagException(0x01); + if (!this.tlv.contains(0x04)) + throw new MissingTagException(0x04); + + this.type = this.tlv.getByte(0x04); + + if (this.type == 0x00) { + try { + this.value = new JSONObject(this.tlv.getString(0x01)); + this.payload = value.getJSONObject("payload"); + this.version = payload.getJSONObject("version"); + + // Ugly, but should work + if (payload.has("isoSalt")) { + this.step = 0x01; + this.step1Data = new Step1Data(payload); + } else if (payload.has("returnCodeMac")) { + this.step = 0x02; + this.step2Data = new Step2Data(payload); + } else if (payload.has("encAuthToken")) { + this.step = 0x03; + this.step3Data = new Step3Data(payload); + } + } catch (JSONException e) { + throw new JsonException("", e); + } + } else { + this.step = 0x04; + this.step4Data = new Step4Data(this.tlv); + } + } + } + + public static class OutgoingRequest extends HuaweiPacket { + public static class Step1Data { + public byte[] isoSalt; + public byte[] peerAuthId; + public byte[] seed; + public int peerUserType; + + String serviceType; // Optional + + public Step1Data(JSONObject payload) throws JSONException { + this.isoSalt = GB.hexStringToByteArray(payload.getString("isoSalt")); + this.peerAuthId = GB.hexStringToByteArray(payload.getString("peerAuthId")); + this.seed = GB.hexStringToByteArray(payload.getString("seed")); + this.peerUserType = payload.getInt("peerUserType"); + + if (payload.has("serviceType")) + this.serviceType = payload.getString("serviceType"); + } + + @Override + public String toString() { + return "Step1Data{" + + "isoSalt=" + StringUtils.bytesToHex(isoSalt) + + ", peerAuthId=" + StringUtils.bytesToHex(peerAuthId) + + ", seed=" + StringUtils.bytesToHex(seed) + + ", peerUserType=" + peerUserType + + ", serviceType='" + serviceType + '\'' + + '}'; + } + } + + public static class Step2Data { + public byte[] peerAuthId; + public byte[] token; + + public boolean isDeviceLevel = false; // Optional + + public Step2Data(JSONObject payload) throws JSONException { + this.peerAuthId = GB.hexStringToByteArray(payload.getString("peerAuthId")); + this.token = GB.hexStringToByteArray(payload.getString("token")); + + if (payload.has("isDeviceLevel")) + this.isDeviceLevel = payload.getBoolean("isDeviceLevel"); + } + + @Override + public String toString() { + return "Step2Data{" + + "peerAuthId=" + StringUtils.bytesToHex(peerAuthId) + + ", token=" + StringUtils.bytesToHex(token) + + ", isDeviceLevel=" + isDeviceLevel + + '}'; + } + } + + public static class Step3Data { + public byte[] nonce; + public byte[] encData; + + public Step3Data(JSONObject payload) throws JSONException { + this.nonce = GB.hexStringToByteArray(payload.getString("nonce")); + this.encData = GB.hexStringToByteArray(payload.getString("encData")); + } + + @Override + public String toString() { + return "Step3Data{" + + "nonce=" + StringUtils.bytesToHex(nonce) + + ", encData=" + StringUtils.bytesToHex(encData) + + '}'; + } + } + + public static class Step4Data { + public byte[] nonce; + public byte[] encResult; + + public Step4Data(JSONObject payload) throws JSONException { + this.nonce = GB.hexStringToByteArray(payload.getString("nonce")); + this.encResult = GB.hexStringToByteArray(payload.getString("encResult")); + } + + @Override + public String toString() { + return "Step4Data{" + + "nonce=" + StringUtils.bytesToHex(nonce) + + ", encResult=" + StringUtils.bytesToHex(encResult) + + '}'; + } + } + + public int step; + + public long requestId; + public byte[] selfAuthId; + public String groupId; + public JSONObject version = null; + public JSONObject payload = null; + public JSONObject value = null; + + public Step1Data step1Data; + public Step2Data step2Data; + public Step3Data step3Data; + public Step4Data step4Data; + + public OutgoingRequest(ParamsProvider paramsProvider) { + super(paramsProvider); + this.complete = false; + } + + @Override + public void parseTlv() throws ParseException { + if (!this.tlv.contains(0x01)) + throw new MissingTagException(0x01); + if (!this.tlv.contains(0x02)) + throw new MissingTagException(0x02); + + try { + value = new JSONObject(this.tlv.getString(0x01)); + payload = value.getJSONObject("payload"); + version = payload.getJSONObject("version"); + + if (payload.has("isoSalt")) { + this.step = 1; + this.step1Data = new Step1Data(payload); + } else if (payload.has("token")) { + this.step = 2; + this.step2Data = new Step2Data(payload); + } else if (payload.has("encData")) { + this.step = 3; + this.step3Data = new Step3Data(payload); + } else if (payload.has("encResult")) { + this.step = 4; + this.step4Data = new Step4Data(payload); + } + } catch (JSONException e) { + throw new JsonException("Cannot parse JSON", e); + } + + if (this.tlv.contains(0x03)) + this.requestId = ByteBuffer.wrap(this.tlv.getBytes(0x03)).getLong(); + } + } + } + + public static class PinCode { + public static final int id = 0x2C; + + public static class Request extends HuaweiPacket { + + public Request(HuaweiPacket.ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01); + this.complete = true; + this.isEncrypted = false; + } + } + + public static class Response extends HuaweiPacket { + public byte[] pinCode; + + public Response(HuaweiPacket.ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + this.isEncrypted = false; + } + + @Override + public void parseTlv() throws ParseException { + byte[] message; + byte[] iv; + if (this.tlv.contains(0x01)) + message = this.tlv.getBytes(0x01); + else + throw new MissingTagException(0x01); + if (this.tlv.contains(0x02)) + iv = this.tlv.getBytes(0x02); + else + throw new MissingTagException(0x02); + HuaweiCrypto huaweiCrypto = new HuaweiCrypto(paramsProvider.getAuthVersion()); + try { + pinCode = huaweiCrypto.decryptPinCode(message, iv); + } catch (HuaweiCrypto.CryptoException e) { + throw new CryptoException("Could not decrypt pinCode", e); + } + } + } + // TODO: implement parsing this request for the log parser support + } + + public static class SettingRelated { + public static final int id = 0x31; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01) + .put(0x02) + .put(0x03) + .put(0x04) + .put(0x05) + .put(0x06); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + } + + @Override + public void parseTlv() throws ParseException { + // Tag 1 -> LegalStuff + // Tag 2 -> File support + // Tag 3 -> SmartWatchVersion + // Tag 4 to 6 are HMS related + } + } + + } + + public static class TimeZoneIdRequest extends HuaweiPacket { + public static final byte id = 0x32; + + public TimeZoneIdRequest( + ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + ByteBuffer timeAndZoneId = ByteBuffer.wrap(HuaweiUtil.getTimeAndZoneId()); + this.tlv = new HuaweiTLV() + .put(0x01, timeAndZoneId.getInt()) + .put(0x02, timeAndZoneId.getShort()); + byte[] zoneId = new byte[timeAndZoneId.remaining()]; + timeAndZoneId.get(zoneId, 0, timeAndZoneId.remaining()); + this.tlv.put(0x03, zoneId); + this.complete = true; + } + + // TODO: implement parsing this request for the log parser support + } + + public static class SecurityNegotiation { + public static final int id = 0x33; + + public static class Request extends HuaweiPacket { + + public Request ( + HuaweiPacket.ParamsProvider paramsProvider, + byte authMode, + byte[] deviceUUID, + String phoneModel) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, authMode); + if (authMode == 0x02 || authMode == 0x04) + this.tlv.put(0x02, (byte)0x01); + this.tlv.put(0x05, deviceUUID) + .put(0x03, (byte)0x01) + .put(0x04, (byte)0x00); + if (authMode == 0x04) + this.tlv.put(0x06) + .put(0x07, phoneModel); + this.complete = true; + this.isEncrypted = false; + } + // TODO: implement parsing this request for the log parser support + } + + public static class Response extends HuaweiPacket { + public int authType; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + } + + @Override + public void parseTlv() throws ParseException { + this.authType = -0x1; + int pw = -0x1; + if (this.tlv.contains(0x01)) { + if (this.tlv.getByte(0x01) == 0x01) + this.authType = 0x0186A0; + if (this.tlv.getByte(0x01) == 0x04) + pw = 4; + } + if (this.tlv.contains(0x02)) { + this.authType = (int)this.tlv.getByte(0x02); + if (pw != -0x1) + this.authType ^= pw; + } + if (this.tlv.contains(0x7F)) + this.authType = (int)this.tlv.getByte(0x7F); + } + } + } + + public static class ConnectStatusRequest extends HuaweiPacket { + public static final int id = 0x35; + + public ConnectStatusRequest (HuaweiPacket.ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01,(byte)0x01); + this.complete = true; + } + // TODO: implement parsing this request for the log parser support + } + + + public static class ExpandCapability { + public static final int id = 0x37; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte[] expandCapabilities; + + 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.expandCapabilities = this.tlv.getBytes(0x01); + } else + throw new MissingTagException(0x01); + } + } + + } + + public static class WearStatus { + public static final int id = 0x3D; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.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); + this.serviceId = DeviceConfig.id; + this.commandId = id; + } + + @Override + public void parseTlv() throws ParseException { + } + } + + } + + public static class SetUpDeviceStatusRequest extends HuaweiPacket { + public static final int id = 0x3E; + + public SetUpDeviceStatusRequest(ParamsProvider paramsProvider, int relationShip, String deviceName) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, (byte) relationShip) + .put(0x02, deviceName) + .put(0x03, (byte)0x00); + + this.complete = true; + } + } + + // TODO: wear location enum? + + public static class Date { + // TODO: enum? + + public static final int yearFirst = 0x01; + public static final int monthFirst = 0x02; + public static final int dayFirst = 0x03; + } + + public static class Time { + // TODO: enum? + + public static final int hours12 = 0x01; + public static final int hours24 = 0x02; + } + + public static class HiChainStep { + public static final int one = 0x01; + public static final int two = 0x02; + public static final int three = 0x03; + public static final int four = 0x04; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DisconnectNotification.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DisconnectNotification.java new file mode 100644 index 000000000..8ebcbcaa4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DisconnectNotification.java @@ -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 . */ +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; + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FindPhone.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FindPhone.java new file mode 100644 index 000000000..701b2bd36 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FindPhone.java @@ -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 . */ +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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FitnessData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FitnessData.java new file mode 100644 index 000000000..e0dc297b2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FitnessData.java @@ -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 . */ +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 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 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 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 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 containers; + + private static final List 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 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/LocaleConfig.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/LocaleConfig.java new file mode 100644 index 000000000..f625ebbbd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/LocaleConfig.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Menstrual.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Menstrual.java new file mode 100644 index 000000000..2d74e383e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Menstrual.java @@ -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 . */ +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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/MusicControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/MusicControl.java new file mode 100644 index 000000000..81ae1b56d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/MusicControl.java @@ -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 . */ +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); + } + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Notifications.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Notifications.java new file mode 100644 index 000000000..2f9e00711 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Notifications.java @@ -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 . */ +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 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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/WorkMode.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/WorkMode.java new file mode 100644 index 000000000..8469996cc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/WorkMode.java @@ -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 . */ +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; + * } + */ +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java new file mode 100644 index 000000000..e95b6d02a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java @@ -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 . */ +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; + + 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 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 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 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; + } + } + + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 7d8a03bb8..acd163f66 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -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), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java index 7cc0e260c..282f3525a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java @@ -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) {} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java new file mode 100644 index 000000000..3ef607468 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java @@ -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 . */ +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 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(); + } + + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java new file mode 100644 index 000000000..c5d2e61a3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java @@ -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 . */ +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 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java new file mode 100644 index 000000000..dad482e32 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java @@ -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 . */ +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 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java new file mode 100644 index 000000000..443129cd2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -0,0 +1,1636 @@ +/* Copyright (C) 2022-2023 Martin.JM + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; +import android.content.SharedPreferences; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import de.greenrobot.dao.query.QueryBuilder; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier.HuaweiDeviceType; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; +import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample; +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.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.entities.Alarm; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +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.model.RecordedDataTypes; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetEventAlarmList; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetNotificationConstraintsRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyRestHeartRateCapabilityRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetAutomaticHeartrateRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetAutomaticSpoRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetDisconnectNotification; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMediumToStrengthThresholdRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.StopFindPhoneRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.StopNotificationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetFitnessTotalsRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetHiChainRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSleepDataCountRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetStepDataCountRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWorkoutCountRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AlarmsRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.DebugRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetActivityTypeRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendAccountRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request.RequestCallback; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetAuthRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetBatteryLevelRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetBondParamsRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetBondRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetConnectStatusRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetDeviceStatusRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetDndLiftWristTypeRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetExpandCapabilityRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetLinkParamsRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPincodeRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetProductInformationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSecurityNegotiationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSettingRelatedRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSupportedServicesRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWearStatusRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendDndAddRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFactoryResetRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFitnessGoalRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualCapabilityRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendDndDeleteRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendSetUpDeviceStatusRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetActivateOnLiftRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetActivityReminderRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetDateFormatRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetLanguageSettingRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetNotificationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetNavigateOnRotateRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetTimeRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetTimeZoneIdRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetTruSleepRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetWearLocationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetWearMessagePushRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetNotificationCapabilitiesRequest; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetWorkModeRequest; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +public class HuaweiSupportProvider { + private static final Logger LOG = LoggerFactory.getLogger(HuaweiSupportProvider.class); + + private HuaweiBRSupport brSupport; + private HuaweiLESupport leSupport; + + private GBDevice gbDevice; + private Context context; + private HuaweiCoordinatorSupplier.HuaweiDeviceType huaweiType; + + private boolean needsAuth = false; + protected byte protocolVersion; + public String deviceMac; //get it from GB + protected String macAddress; + protected String androidID; + protected short msgId = 0; + + private MusicStateSpec musicStateSpec = null; + private MusicSpec musicSpec = null; + + private final HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider(); + + protected ResponseManager responseManager = new ResponseManager(this); + + public HuaweiCoordinatorSupplier getCoordinator() { + return ((HuaweiCoordinatorSupplier) this.gbDevice.getDeviceCoordinator()); + } + + public HuaweiCoordinator getHuaweiCoordinator() { + return getCoordinator().getHuaweiCoordinator(); + } + + public HuaweiSupportProvider(HuaweiBRSupport support) { + this.brSupport = support; + } + + public HuaweiSupportProvider(HuaweiLESupport support) { + this.leSupport = support; + } + + public boolean isBLE() { + return huaweiType == HuaweiDeviceType.AW || huaweiType == HuaweiDeviceType.BLE || huaweiType == HuaweiDeviceType.SMART; + } + + public nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder createLeTransactionBuilder(String taskName) { + return leSupport.createTransactionBuilder(taskName); + } + + public nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder createBrTransactionBuilder(String taskName) { + return brSupport.createTransactionBuilder(taskName); + } + + public BluetoothGattCharacteristic getLeCharacteristic(UUID uuid) { + return leSupport.getCharacteristic(uuid); + } + + public void performConnected(nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction transaction) throws IOException { + leSupport.performConnected(transaction); + } + + public void performConnected(nodomain.freeyourgadget.gadgetbridge.service.btbr.Transaction transaction) throws IOException { + brSupport.performConnected(transaction); + } + + public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) { + if (isBLE()) { + leSupport.evaluateGBDeviceEvent(deviceEvent); + } else { + brSupport.evaluateGBDeviceEvent(deviceEvent); + } + } + + protected nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder initializeDevice(nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) { + this.gbDevice = leSupport.getDevice(); + this.context = leSupport.getContext(); + this.huaweiType = getCoordinator().getHuaweiType(); + this.paramsProvider.setTransactionsCrypted(this.getHuaweiCoordinator().isTransactionCrypted()); + builder.setCallback(leSupport); + builder.notify(leSupport.getCharacteristic(HuaweiConstants.UUID_CHARACTERISTIC_HUAWEI_READ), true); + builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); + final GetLinkParamsRequest linkParamsReq = new GetLinkParamsRequest(this, builder); + initializeDevice(linkParamsReq); + getCoordinator().setDevice(this.gbDevice); + return builder; + } + + protected nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder initializeDevice(nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder) { + this.gbDevice = brSupport.getDevice(); + this.context = brSupport.getContext(); + this.huaweiType = getCoordinator().getHuaweiType(); + this.paramsProvider.setTransactionsCrypted(this.getHuaweiCoordinator().isTransactionCrypted()); + builder.setCallback(brSupport); + builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); + final GetLinkParamsRequest linkParamsReq = new GetLinkParamsRequest(this, builder); + initializeDevice(linkParamsReq); + getCoordinator().setDevice(this.gbDevice); + return builder; + } + + protected void initializeDevice(Request linkParamsReq) { + deviceMac = this.gbDevice.getAddress(); + createRandomMacAddress(); + createAndroidID(); + try { + RequestCallback finalizeReq = new RequestCallback() { + @Override + public void call() { + initializeDeviceCheckStatus(linkParamsReq); + } + + @Override + public void handleException(Request.ResponseParseException e) { + LOG.error("Link params TLV exception", e); + } + }; + linkParamsReq.setFinalizeReq(finalizeReq); + linkParamsReq.doPerform(); + } catch (IOException e) { + GB.toast(context, "Initialization of authenticating to Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Initialization of authenticating to Huawei device failed", e); + } + + /* This is to have the setting match the default Huawei behaviour */ + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(getDeviceMac()); + if (!sharedPrefs.contains(DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED)) { + sharedPrefs.edit().putBoolean(DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED, true).apply(); + } + } + + protected void initializeDeviceCheckStatus(Request linkParamsReq) { + try { + GetDeviceStatusRequest deviceStatusReq = new GetDeviceStatusRequest(this, true); + RequestCallback finalizeReq = new RequestCallback() { + @Override + public void call() { + int status = (int)deviceStatusReq.status; + if (status == -0x01 || status == 0x00 || status == 0x01) { + initializeDeviceDealHiChain(linkParamsReq); + } else { + initializeDeviceNotify(); + } + } + + @Override + public void handleException(Request.ResponseParseException e) { + LOG.error("Status TLV exception", e); + } + }; + if (huaweiType == HuaweiDeviceType.BLE) { //Only BLE known, check later for AW and SMART + initializeDeviceDealHiChain(linkParamsReq); + } else { + deviceStatusReq.setFinalizeReq(finalizeReq); + deviceStatusReq.doPerform(); + } + } catch (IOException e) { + GB.toast(context, "Status of authenticating to Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + e.printStackTrace(); + } + } + + protected boolean isHiChain() { + // In HH + // HiChain : 1 || 3 + // HiChainLite : 2 || 3 || 8 + // HiChain3 : 4 & API>=23 - API is always >=23 + // For GB we will consider for authMode + // 0 : No HiChain + // 1 or 3 : HiChain + // 2 or 8 : HiChainLite -> normal mode + // 4 : HiChain3 + byte authMode = paramsProvider.getAuthMode(); + return authMode == 0x01 || authMode == 0x03 || authMode == 0x04 || isHiChainLite(); + } + + protected boolean isHiChainLite() { + byte authMode = paramsProvider.getAuthMode(); + return authMode == 0x02; + } + + protected boolean isHiChain3(int authType) { + return (authType ^ 0x01) == 0x04 || (authType ^ 0x02) == 0x04; + } + + protected void initializeDeviceDealHiChain(Request linkParamsReq) { + try { + if (isHiChain()) { + GetSecurityNegotiationRequest securityNegoReq = new GetSecurityNegotiationRequest(this); + RequestCallback securityFinalizeReq = new RequestCallback(this) { + @Override + public void call() { + if (securityNegoReq.authType == 0x0186A0 || isHiChain3(securityNegoReq.authType)) { + LOG.debug("HiChain mode"); + initializeDeviceHiChainMode(linkParamsReq); + } else if (securityNegoReq.authType == 0x01 || securityNegoReq.authType == 0x02) { + LOG.debug("HiChain Lite mode"); + initializeDeviceHiChainLiteMode(linkParamsReq); + } + } + }; + securityNegoReq.setFinalizeReq(securityFinalizeReq); + securityNegoReq.doPerform(); + } else { + LOG.debug("Normal mode"); + initializeDeviceNormalMode(linkParamsReq); + } + } catch (IOException e) { + // TODO: use translatable string + GB.toast(context, "init Deal with HiChain of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Step of authenticating to Huawei device failed", e); + } + } + + protected void initializeDeviceNotify() {} //TODO + + RequestCallback configureReq = new RequestCallback() { + @Override + public void call() { + initializeDeviceConfigure(); + } + }; + + protected void initializeDeviceHiChainMode(Request linkParamsReq) { + try { + GetHiChainRequest hiChainReq = new GetHiChainRequest(this, needsAuth); + hiChainReq.setFinalizeReq(configureReq); + if (((GetLinkParamsRequest)linkParamsReq).bondState == 0x00 || ((GetLinkParamsRequest)linkParamsReq).bondState == 0x02) { + GetPincodeRequest pincodeReq = new GetPincodeRequest(this); + pincodeReq.nextRequest(hiChainReq); + pincodeReq.doPerform(); + } else + hiChainReq.doPerform(); + } catch (IOException e) { + GB.toast(context, "init HiCHain Mode of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + e.printStackTrace(); + } + } + + protected void initializeDeviceHiChainLiteMode(Request linkParamsReq) { + try { + createSecretKey(); + GetAuthRequest authReq = new GetAuthRequest(this, linkParamsReq, true); + GetBondParamsRequest bondParamsReq = new GetBondParamsRequest(this); + GetBondRequest bondReq = new GetBondRequest(this); + authReq.nextRequest(bondParamsReq); + bondParamsReq.nextRequest(bondReq); + bondParamsReq.setFinalizeReq(configureReq); + bondReq.setFinalizeReq(configureReq); + if (paramsProvider.getPinCode() == null & paramsProvider.getAuthVersion() != 0x02) { + GetPincodeRequest pinCodeReq = new GetPincodeRequest(this); + pinCodeReq.nextRequest(authReq); + pinCodeReq.doPerform(); + } else { + authReq.doPerform(); + } + } catch (IOException e) { + GB.toast(context, "init HiCHainLite Mode Mode of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + e.printStackTrace(); + } + } + + protected void initializeDeviceNormalMode(Request linkParamsReq) { + try { + createSecretKey(); + GetAuthRequest authReq = new GetAuthRequest(this, linkParamsReq); + GetBondParamsRequest bondParamsReq = new GetBondParamsRequest(this); + GetBondRequest bondReq = new GetBondRequest(this); + authReq.nextRequest(bondParamsReq); + bondParamsReq.nextRequest(bondReq); + bondParamsReq.setFinalizeReq(configureReq); + bondReq.setFinalizeReq(configureReq); + authReq.doPerform(); + } catch (IOException e) { + GB.toast(context, "init Normal Mode of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + e.printStackTrace(); + } + + } + + protected void initializeDeviceConfigure() { + nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder leBuilder = null; + nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder brBuilder = null; + if (isBLE()) { + leBuilder = createLeTransactionBuilder("Initializing"); + leBuilder.setCallback(leSupport); + leBuilder.notify(leSupport.getCharacteristic(HuaweiConstants.UUID_CHARACTERISTIC_HUAWEI_READ), true); + leBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction(gbDevice, GBDevice.State.INITIALIZING, context)); + } else { + brBuilder = createBrTransactionBuilder("Initializing"); + brBuilder.setCallback(brSupport); + brBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(gbDevice, GBDevice.State.INITIALIZING, context)); + } + try { + GetProductInformationRequest productInformationReq = new GetProductInformationRequest(this); + Request setTimeReq = setTime(); + GetSupportedServicesRequest supportedServicesReq = new GetSupportedServicesRequest(this); + productInformationReq.nextRequest(setTimeReq); + setTimeReq.nextRequest(supportedServicesReq); + productInformationReq.doPerform(); + if (needsAuth) { + // Workaround to enable PREF_HUAWEI_ROTATE_WRIST_TO_SWITCH_INFO preference + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac); + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putString(DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT, "p_on"); + editor.apply(); +// initializeAlarms(); + setNavigateOnRotate(); + setTrusleep(); + } + onSetTime(); + getBatteryLevel(); + if (isBLE()) { + assert leBuilder != null; + leBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction(gbDevice, GBDevice.State.INITIALIZED, context)); + leSupport.performConnected(leBuilder.getTransaction()); + } else { + assert brBuilder != null; + brBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(gbDevice, GBDevice.State.INITIALIZED, context)); + brSupport.performConnected(brBuilder.getTransaction()); + } + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Final initialization of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Final initialization of Huawei device failed", e); + } + } + + public void createSecretKey() { + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac); + + String authKey = sharedPrefs.getString("authkey", null); + if (authKey == null || authKey.isEmpty()) { + SharedPreferences.Editor editor = sharedPrefs.edit(); + + authKey = StringUtils.bytesToHex(HuaweiCrypto.generateNonce()); + editor.putString("authkey", authKey); + editor.apply(); + } + paramsProvider.setSecretKey(GB.hexStringToByteArray(authKey)); + } + + public byte[] getSecretKey() { + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac); + + String authKey = sharedPrefs.getString("authkey", null); + return GB.hexStringToByteArray(authKey); + } + + public void setSecretKey(byte[] authKey) { + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac); + + SharedPreferences.Editor editor = sharedPrefs.edit(); + + editor.putString("authkey", StringUtils.bytesToHex(authKey)); + editor.apply(); + paramsProvider.setSecretKey(authKey); + } + + public HuaweiCoordinatorSupplier.HuaweiDeviceType getHuaweiType() { + return this.huaweiType; + } + + public HuaweiPacket.ParamsProvider getParamsProvider() { + return paramsProvider; + } + + public void setNeedsAuth(boolean needsAuth) { + this.needsAuth = needsAuth; + } + + protected void createRandomMacAddress() { + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac); + + macAddress = sharedPrefs.getString(HuaweiConstants.PREF_HUAWEI_ADDRESS, null); + if (macAddress == null || macAddress.isEmpty()) { + StringBuilder mac = new StringBuilder("FF:FF:FF"); + Random r = new Random(); + for (int i = 0; i < 3; i++) { + int n = r.nextInt(255); + mac.append(String.format(":%02x", n)); + } + macAddress = mac.toString().toUpperCase(); + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putString(HuaweiConstants.PREF_HUAWEI_ADDRESS, macAddress); + editor.apply(); + } + } + + public byte[] getMacAddress() { + return macAddress.getBytes(StandardCharsets.UTF_8); + } + + public byte[] getSerial() { + return macAddress.replace(":", "").substring(6, 12).getBytes(StandardCharsets.UTF_8); + } + + public String getDeviceMac() { + return deviceMac; + } + + protected void createAndroidID() { + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac); + + androidID = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_FAKE_ANDROID_ID, null); + if (androidID == null || androidID.isEmpty()) { + androidID = StringUtils.bytesToHex(HuaweiCrypto.generateNonce()); + LOG.debug("Created androidID: " + androidID); + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putString(DeviceSettingsPreferenceConst.PREF_FAKE_ANDROID_ID, androidID); + editor.apply(); + } + } + + public byte[] getAndroidId() { + return androidID.getBytes(StandardCharsets.UTF_8); + } + + public Context getContext() { + return context; + } + + public GBDevice getDevice() { + return gbDevice; + } + + /** + * Initialize the services that may or may not be supported on the device + * To be called after the commandsPerService is filled in the coordinator + */ + public void initializeDynamicServices() { + + // Setup the alarms + if (!getHuaweiCoordinator().supportsChangingAlarm()) { + if (needsAuth) { + // TODO: not really sure if this is necessary, but it probably won't do any harm + initializeAlarms(); + } + } else { + getAlarms(); + } + try { + if (getHuaweiCoordinator().supportsExpandCapability()) { + GetExpandCapabilityRequest expandCapabilityReq = new GetExpandCapabilityRequest(this); + expandCapabilityReq.doPerform(); + } + if (getHuaweiCoordinator().supportsAccount()) { // GetAccountJudgment + SendAccountRequest sendAccountReq = new SendAccountRequest(this); + sendAccountReq.doPerform(); + } + if (getHuaweiCoordinator().supportsActivityType()) { + GetActivityTypeRequest activityTypeReq = new GetActivityTypeRequest(this); + activityTypeReq.doPerform(); + } + if (getHuaweiCoordinator().supportsSettingRelated()) { // GetSettingRelated + GetSettingRelatedRequest getSettingRelatedReq = new GetSettingRelatedRequest(this); + getSettingRelatedReq.doPerform(); + } + if (getHuaweiCoordinator().supportsConnectStatus()) { + GetConnectStatusRequest getConnectStatusReq = new GetConnectStatusRequest(this); + getConnectStatusReq.doPerform(); + } + if (getHuaweiCoordinator().supportsActivateOnLift()) { + setActivateOnLift(); + } + if (getHuaweiCoordinator().supportsWearLocation(getDevice())) { + setWearLocation(); + } + if (getHuaweiCoordinator().supportsQueryDndLiftWristDisturbType()) { + GetDndLiftWristTypeRequest getDndLiftWristTypeReq = new GetDndLiftWristTypeRequest(this); + getDndLiftWristTypeReq.doPerform(); + } + if (getHuaweiCoordinator().supportsDoNotDisturb(gbDevice)) { + SendDndDeleteRequest sendDndDeleteReq = new SendDndDeleteRequest(this); + SendDndAddRequest sendDndAddReq = new SendDndAddRequest(this); + sendDndDeleteReq.nextRequest(sendDndAddReq); + sendDndDeleteReq.doPerform(); + } + if (getHuaweiCoordinator().supportsNotification()) { // 0x02 - 0x04 + setNotificationStatus(); + } + if (getHuaweiCoordinator().supportsDoNotDisturb(gbDevice) && getHuaweiCoordinator().supportsWearMessagePush()) { + setDndNotWear(); + } + if (getHuaweiCoordinator().supportsTimeAndZoneId()) { + setTimeZoneId(); + } + // Nothing usefull yet with this requests + if (getHuaweiCoordinator().supportsMultiDevice()) { + SendSetUpDeviceStatusRequest sendSetUpDeviceStatusReq = new SendSetUpDeviceStatusRequest(this); + sendSetUpDeviceStatusReq.doPerform(); + GetWearStatusRequest getWearStatusReq = new GetWearStatusRequest(this); + getWearStatusReq.doPerform(); + } + if (getHuaweiCoordinator().supportsMenstrual()) { + SendMenstrualCapabilityRequest sendMenstrualCapabilityReq = new SendMenstrualCapabilityRequest(this); + sendMenstrualCapabilityReq.doPerform(); + } + if (getHuaweiCoordinator().supportsLanguageSetting()) { // 0x0c - 0x01 + setLanguageSetting(); + } + if (getHuaweiCoordinator().supportsWorkoutsTrustHeartRate()) { + SendNotifyHeartRateCapabilityRequest sendNotifyHeartRateCapabilityReq = new SendNotifyHeartRateCapabilityRequest(this); + sendNotifyHeartRateCapabilityReq.doPerform(); + } + if (getHuaweiCoordinator().supportsFitnessRestHeartRate()) { + SendNotifyRestHeartRateCapabilityRequest sendNotifyRestHeartRateCapabilityReq = new SendNotifyRestHeartRateCapabilityRequest(this); + sendNotifyRestHeartRateCapabilityReq.doPerform(); + } + if (getHuaweiCoordinator().supportsFitnessThresholdValue()) { + SetMediumToStrengthThresholdRequest setMediumToStrengthThresholdReq = new SetMediumToStrengthThresholdRequest(this); + setMediumToStrengthThresholdReq.doPerform(); + } + if (getHuaweiCoordinator().supportsDateFormat()) { //0x01 - 0x04 + setDateFormat(); + } + if (getHuaweiCoordinator().supportsMotionGoal()) { + SendFitnessGoalRequest sendFitnessGoalReq = new SendFitnessGoalRequest(this); + sendFitnessGoalReq.doPerform(); + } + if (getHuaweiCoordinator().supportsActivityReminder()) { + setActivityReminder(); + } + if (getHuaweiCoordinator().supportsPromptPushMessage() && getProtocolVersion() == 2) { + GetNotificationCapabilitiesRequest getNotificationCapabilitiesReq = new GetNotificationCapabilitiesRequest(this); + getNotificationCapabilitiesReq.doPerform(); + } + if (getHuaweiCoordinator().supportsNotificationAlert() && getProtocolVersion() == 2) { + GetNotificationConstraintsRequest getNotificationConstraintsReq = new GetNotificationConstraintsRequest(this); + getNotificationConstraintsReq.doPerform(); + } + } catch (IOException e) { + GB.toast(getContext(), "Initialize dynamic services of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, + e); + e.printStackTrace(); + } + + // Properly update the device card + gbDevice.sendDeviceUpdateIntent(GBApplication.getContext()); + GB.signalActivityDataFinish(); + } + + public void setProtocolVersion(byte protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public byte getProtocolVersion() { + return this.protocolVersion; + } + + private void initializeAlarms() { + // TODO: check for smart alarm && overwrite for smart alarm + // note that lowering the alarm count shouldn't delete the alarm of course... + + // Populate alarms in order to specify important data + List alarms = DBHelper.getAlarms(gbDevice); + DeviceCoordinator coordinator = this.gbDevice.getDeviceCoordinator(); + int supportedNumAlarms = coordinator.getAlarmSlotCount(gbDevice); + if (alarms.size() == 0) { + try (DBHandler db = GBApplication.acquireDB()) { + DaoSession daoSession = db.getDaoSession(); + Device device = DBHelper.getDevice(gbDevice, daoSession); + User user = DBHelper.getUser(daoSession); + for (int position = 0; position < supportedNumAlarms; position++) { + LOG.info("adding missing alarm at position " + position); + DBHelper.store(createDefaultAlarm(device, user, position)); + } + } catch (Exception e) { + // TODO: show user? + // TODO: What exceptions can happen here? + LOG.error("Error accessing database", e); + } + } + } + + private Alarm createDefaultAlarm(@NonNull Device device, @NonNull User user, int position) { + boolean smartWakeup = false; + String title = context.getString(R.string.menuitem_alarm); + String description = context.getString(R.string.huawei_alarm_event_description); + if (position == 0) { + smartWakeup = true; + title = context.getString(R.string.alarm_smart_wakeup); + description = context.getString(R.string.huawei_alarm_smart_description); + } + return new Alarm(device.getId(), user.getId(), position, false, smartWakeup, false, 0, 6, 30, true, title, description); + } + + private void getAlarms() { + if (!getHuaweiCoordinator().supportsChangingAlarm()) + return; + + GetEventAlarmList getEventAlarmList = new GetEventAlarmList(this); + responseManager.addHandler(getEventAlarmList); + getEventAlarmList.setFinalizeReq(new RequestCallback() { + @Override + public void call() { + if (!getHuaweiCoordinator().supportsSmartAlarm(getDevice())) + return; // Don't get smart alarms when not supported + + GetSmartAlarmList getSmartAlarmList = new GetSmartAlarmList(HuaweiSupportProvider.this); + responseManager.addHandler(getSmartAlarmList); + try { + getSmartAlarmList.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Error sending smart alarm list request", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Error sending smart alarm list request", e); + } + } + + @Override + public void handleException(Request.ResponseParseException e) { + // TODO: Use translatable string + GB.toast(context, "Error parsing event list", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Error parsing event list", e); + } + }); + try { + getEventAlarmList.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Error sending event alarm list request", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Error sending event alarm list request", e); + } + } + + public void saveAlarms(Alarm[] alarms) { + try (DBHandler db = GBApplication.acquireDB()) { + DaoSession daoSession = db.getDaoSession(); + Device device = DBHelper.getDevice(gbDevice, daoSession); + User user = DBHelper.getUser(daoSession); + for (Alarm alarm : alarms) { + alarm.setDeviceId(device.getId()); + alarm.setUserId(user.getId()); + DBHelper.store(alarm); + } + } catch (Exception e) { + // TODO: Use translatable string + GB.toast(context, "Error saving alarms", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Error saving alarms", e); + } + } + + public boolean onCharacteristicChanged(BluetoothGattCharacteristic characteristic) { + byte[] data = characteristic.getValue(); + responseManager.handleData(data); + return true; + } + + public void onSocketRead(byte[] data) { + //Check multiple packet in data + ByteBuffer bData = ByteBuffer.wrap(data); + while (bData.remaining() != 0x00) { + int dataLen = bData.getShort(bData.position() + 1) + 0x05; // magic + len + CRC + byte[] newData = new byte[dataLen]; + bData.get(newData, 0, dataLen); + responseManager.handleData(newData); + } + } + + public void removeInProgressRequests(Request req) { + responseManager.removeHandler(req); + } + + public void onSendConfiguration(String config) { + try { + switch (config) { + case DeviceSettingsPreferenceConst.PREF_DATEFORMAT: + case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT: { + setDateFormat(); + break; + } + case SettingsActivity.PREF_MEASUREMENT_SYSTEM: + case DeviceSettingsPreferenceConst.PREF_LANGUAGE: { + setLanguageSetting(); + break; + } + case DeviceSettingsPreferenceConst.PREF_WEARLOCATION: { + setWearLocation(); + break; + } + case DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED: { + setActivateOnLift(); + break; + } + case MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO: { + setNavigateOnRotate(); + break; + } + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_START: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_END: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_MO: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_TU: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_WE: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_TH: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_FR: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_SA: + case DeviceSettingsPreferenceConst.PREF_INACTIVITY_SU: { + setActivityReminder(); + break; + } + case HuaweiConstants.PREF_HUAWEI_TRUSLEEP: { + setTrusleep(); + break; + } + case DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE: { + setNotificationStatus(); + break; + } + case HuaweiConstants.PREF_HUAWEI_WORKMODE: + SetWorkModeRequest setWorkModeReq = new SetWorkModeRequest(this); + setWorkModeReq.doPerform(); + break; + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_START: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_END: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_LIFT_WRIST: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_MO: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_TU: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_WE: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_TH: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_FR: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SA: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SU: { + setDnd(); + break; + } + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOT_WEAR: + setDndNotWear(); + break; + case DeviceSettingsPreferenceConst.PREF_FIND_PHONE: + case DeviceSettingsPreferenceConst.PREF_FIND_PHONE_DURATION: + // TODO: enable/disable the find phone applet on band + break; + case DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED: + setDisconnectNotification(); + break; + case DeviceSettingsPreferenceConst.PREF_HEARTRATE_AUTOMATIC_ENABLE: + setHeartrateAutomatic(); + break; + case DeviceSettingsPreferenceConst.PREF_SPO_AUTOMATIC_ENABLE: + setSpoAutomatic(); + break; + case DeviceSettingsPreferenceConst.PREF_FORCE_ENABLE_SMART_ALARM: + getAlarms(); + break; + case HuaweiConstants.PREF_HUAWEI_DEBUG_REQUEST: + sendDebugRequest(); + break; + case ActivityUser.PREF_USER_STEPS_GOAL: + new SendFitnessGoalRequest(this).doPerform(); + break; + } + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Configuration of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Configuration of Huawei device failed", e); + // TODO: handle this? + } + } + + public void onFetchRecordedData(int dataTypes) { + if (gbDevice.isBusy()) { + LOG.warn("Device is already busy with " + gbDevice.getBusyTask() + ", so won't fetch data now."); + // TODO: better way of letting user know? + // TODO: use string that can be translated + GB.toast("Device is already busy with " + gbDevice.getBusyTask() + ", so won't fetch data now.", Toast.LENGTH_LONG, 0); + return; + } + + // TODO: An exception during the parsing can leave GB thinking that the sync is not yet + // finished, but it won't ever complete because of the parsing exception + // Maybe this can be fixed with an exception handler from the callback? If then + // called from the ResponseManager, it may not be too much work to implement. + + if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) { + fetchActivityData(); + } else if (dataTypes == RecordedDataTypes.TYPE_GPS_TRACKS) { + fetchWorkoutData(); + } else { + // TODO: tell user + LOG.warn("Recorded data type {} not implemented yet.", dataTypes); + } + + // Get the battery level, as that isn't shared nicely for now + getBatteryLevel(); + + // Get the alarms as they cannot be retrieved on opening the alarm window + // TODO: get the alarms if the alarm settings are opened instead of here + getAlarms(); + } + + private void fetchActivityData() { + int sleepStart = 0; + int stepStart = 0; + int end = (int) (System.currentTimeMillis() / 1000); + + SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()); + long prefLastSyncTime = sharedPreferences.getLong("lastSyncTimeMillis", 0); + if (prefLastSyncTime != 0) { + sleepStart = (int) (prefLastSyncTime / 1000); + stepStart = (int) (prefLastSyncTime / 1000); + + // Reset for next calls + sharedPreferences.edit().putLong("lastSyncTimeMillis", 0).apply(); + } else { + try (DBHandler db = GBApplication.acquireDB()) { + HuaweiSampleProvider sampleProvider = new HuaweiSampleProvider(gbDevice, db.getDaoSession()); + sleepStart = sampleProvider.getLastSleepFetchTimestamp(); + stepStart = sampleProvider.getLastStepFetchTimestamp(); + } catch (Exception e) { + LOG.warn("Exception for getting start times, using 01/01/2000 - 00:00:00."); + } + + // Some bands don't work with zero timestamp, so starting later + if (sleepStart == 0) + sleepStart = 946684800; + if (stepStart == 0) + stepStart = 946684800; + } + final GetSleepDataCountRequest getSleepDataCountRequest; + if (isBLE()) { + nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder leBuilder = createLeTransactionBuilder("FetchRecordedData"); + leBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction(gbDevice, context.getString(R.string.busy_task_fetch_activity_data), context)); + getSleepDataCountRequest = new GetSleepDataCountRequest(this, leBuilder, sleepStart, end); + } else { + nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder brBuilder = createBrTransactionBuilder("FetchRecordedData"); + brBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceBusyAction(gbDevice, context.getString(R.string.busy_task_fetch_activity_data), context)); + getSleepDataCountRequest = new GetSleepDataCountRequest(this, brBuilder, sleepStart, end); + } + + final GetStepDataCountRequest getStepDataCountRequest = new GetStepDataCountRequest(this, stepStart, end); + final GetFitnessTotalsRequest getFitnessTotalsRequest = new GetFitnessTotalsRequest(this); + + getFitnessTotalsRequest.setFinalizeReq(new RequestCallback() { + @Override + public void call() { + handleSyncFinished(); + } + + @Override + public void handleException(Request.ResponseParseException e) { + LOG.error("Fitness totals exception", e); + handleSyncFinished(); + } + }); + + getStepDataCountRequest.setFinalizeReq(new RequestCallback() { + @Override + public void call() { + try { + getFitnessTotalsRequest.doPerform(); + } catch (IOException e) { + LOG.error("Exception on starting fitness totals request", e); + handleSyncFinished(); + } + } + + @Override + public void handleException(Request.ResponseParseException e) { + LOG.error("Step data count exception", e); + handleSyncFinished(); + } + }); + + getSleepDataCountRequest.setFinalizeReq(new RequestCallback() { + @Override + public void call() { + try { + getStepDataCountRequest.doPerform(); + } catch (IOException e) { + LOG.error("Exception on starting step data count request", e); + handleSyncFinished(); + } + } + + @Override + public void handleException(Request.ResponseParseException e) { + LOG.error("Sleep data count exception", e); + handleSyncFinished(); + } + }); + + try { + getSleepDataCountRequest.doPerform(); + } catch (IOException e) { + LOG.error("Exception on starting sleep data count request", e); + handleSyncFinished(); + } + } + + private void fetchWorkoutData() { + int start = 0; + int end = (int) (System.currentTimeMillis() / 1000); + + SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()); + long prefLastSyncTime = sharedPreferences.getLong("lastSportsActivityTimeMillis", 0); + if (prefLastSyncTime != 0) { + start = (int) (prefLastSyncTime / 1000); + + // Reset for next calls + sharedPreferences.edit().putLong("lastSportsActivityTimeMillis", 0).apply(); + } else { + try (DBHandler db = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId(); + + QueryBuilder qb1 = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where( + HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId), + HuaweiWorkoutSummarySampleDao.Properties.UserId.eq(userId) + ).orderDesc( + HuaweiWorkoutSummarySampleDao.Properties.StartTimestamp + ).limit(1); + + List samples1 = qb1.list(); + if (!samples1.isEmpty()) + start = samples1.get(0).getEndTimestamp(); + + QueryBuilder qb2 = db.getDaoSession().getBaseActivitySummaryDao().queryBuilder().where( + BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId), + BaseActivitySummaryDao.Properties.UserId.eq(userId) + ).orderDesc( + BaseActivitySummaryDao.Properties.StartTime + ).limit(1); + + List samples2 = qb2.list(); + if (!samples2.isEmpty()) + start = Math.min(start, (int) (samples2.get(0).getEndTime().getTime() / 1000L)); + + start = start + 1; + } catch (Exception e) { + LOG.warn("Exception for getting start time, using 10/06/2022 - 00:00:00."); + } + + if (start == 0 || start == 1) + start = 1654819200; + } + + final GetWorkoutCountRequest getWorkoutCountRequest; + if (isBLE()) { + nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder leBuilder = createLeTransactionBuilder("FetchWorkoutData"); + // TODO: maybe use a different string from the other synchronization + leBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction(gbDevice, context.getString(R.string.busy_task_fetch_activity_data), context)); + getWorkoutCountRequest = new GetWorkoutCountRequest(this, leBuilder, start, end); + } else { + nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder brBuilder = createBrTransactionBuilder("FetchWorkoutData"); + // TODO: maybe use a different string from the other synchronization + brBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceBusyAction(gbDevice, context.getString(R.string.busy_task_fetch_activity_data), context)); + getWorkoutCountRequest = new GetWorkoutCountRequest(this, brBuilder, start, end); + } + + getWorkoutCountRequest.setFinalizeReq(new RequestCallback() { + @Override + public void call() { + handleSyncFinished(); + } + + @Override + public void handleException(Request.ResponseParseException e) { + LOG.error("Workout parsing exception", e); + handleSyncFinished(); + } + }); + + try { + getWorkoutCountRequest.doPerform(); + } catch (IOException e) { + LOG.error("Exception on starting workout count request", e); + handleSyncFinished(); + } + } + + private void handleSyncFinished() { + if (gbDevice.isBusy()) { + gbDevice.unsetBusyTask(); + gbDevice.sendDeviceUpdateIntent(context); + } + GB.signalActivityDataFinish(); + } + + public void onReset(int flags) { + try { + if(flags== GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) { + SendFactoryResetRequest sendFactoryResetReq = new SendFactoryResetRequest(this); + sendFactoryResetReq.doPerform(); + } + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Factory resetting Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Factory resetting Huawei device failed", e); + } + } + + public void setNotificationStatus() { + /* + * TODO: this doesn't work as expected + * We thought it would disable(/enable) the notifications on the device side, + * but at least the disabling doesn't work - so we don't send notifications to the + * device at all if the setting is disabled now. + * TRYING to debug this as it should really be handled on device side... + */ + try { + SetNotificationRequest setNotificationReq = new SetNotificationRequest(this); + setNotificationReq.doPerform(); +// SetWearMessagePushRequest setWearMessagePushReq = new SetWearMessagePushRequest(this); +// setWearMessagePushReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Setting notification failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Setting notification failed", e); + } + } + + public short getNotificationId() { + if (msgId < 256) { + msgId += 1; + } else { + msgId = 0; + } + return msgId; + } + + public void onNotification(NotificationSpec notificationSpec) { + if (!GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false)) { + // Don't send notifications when they are disabled + LOG.info("Stopped notification as they are disabled."); + return; + } + + SendNotificationRequest sendNotificationReq = new SendNotificationRequest(this); + try { + sendNotificationReq.buildNotificationTLVFromNotificationSpec(notificationSpec); + sendNotificationReq.doPerform(); + } catch (IOException e) { + LOG.error("Sending notification failed", e); + } + } + + public void setDateFormat() { + try { + SetDateFormatRequest setDateFormatReq = new SetDateFormatRequest(this); + setDateFormatReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure date format", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure date format", e); + } + } + + public void onSetTime() { + try { + setTime().doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure time", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure time", e); + } + } + + private Request setTime() { + SetTimeRequest setTimeReq = new SetTimeRequest(this); + return setTimeReq; + } + + public void setTimeZoneId() { + try { + SetTimeZoneIdRequest setTimeZoneIdReq = new SetTimeZoneIdRequest(this); + setTimeZoneIdReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure time and zoneId", Toast.LENGTH_SHORT, GB.ERROR, e); + } + } + + public void onSetAlarms(ArrayList alarms) { + boolean smartAlarmEnabled = getHuaweiCoordinator().supportsSmartAlarm(getDevice()); + + AlarmsRequest smartAlarmReq = new AlarmsRequest(this, true); + AlarmsRequest eventAlarmReq = new AlarmsRequest(this, false); + for (nodomain.freeyourgadget.gadgetbridge.model.Alarm alarm : alarms) { + if (alarm.getPosition() == 0 && smartAlarmEnabled) { + smartAlarmReq.buildSmartAlarm(alarm); + } else { + eventAlarmReq.addEventAlarm(alarm, !smartAlarmEnabled); + } + } + try { + if (smartAlarmEnabled) + smartAlarmReq.doPerform(); + eventAlarmReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure alarms", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure alarms", e); + } + } + + public void onSetCallState(CallSpec callSpec) { + if (callSpec.command == CallSpec.CALL_INCOMING) { + SendNotificationRequest sendNotificationReq = new SendNotificationRequest(this); + try { + sendNotificationReq.buildNotificationTLVFromCallSpec(callSpec); + sendNotificationReq.doPerform(); + } catch (IOException e) { + LOG.error("Failed to send start call notification", e); + } + } else if ( + callSpec.command == CallSpec.CALL_ACCEPT || + callSpec.command == CallSpec.CALL_START || + callSpec.command == CallSpec.CALL_REJECT || + callSpec.command == CallSpec.CALL_END + ) { + StopNotificationRequest stopNotificationRequest = new StopNotificationRequest(this); + try { + stopNotificationRequest.doPerform(); + } catch (IOException e) { + LOG.error("Failed to send stop call notification", e); + } + } + } + + public void onSetMusicState(MusicStateSpec stateSpec) { + this.musicStateSpec = stateSpec; + sendSetMusic(); + } + + public void onSetMusicInfo(MusicSpec musicSpec) { + this.musicSpec = musicSpec; + sendSetMusic(); + } + + public void onSetPhoneVolume() { + // TODO: check when implemented in GB + + // We get the audio volume manually, so ignoring the argument + sendSetMusic(); + } + + public void sendSetMusic() { + // This often gets called twice in a row because of onSetMusicState and onSetMusicInfo + // Maybe we can consolidate that into just one request? + SetMusicRequest setMusicRequest = new SetMusicRequest(this, this.musicStateSpec, this.musicSpec); + try { + setMusicRequest.doPerform(); + } catch (IOException e) { + LOG.error("Failed to send set music request", e); + } + } + + public void addInProgressRequest(Request request) { + responseManager.addHandler(request); + } + + public void addSleepActivity(int timestamp, short duration, byte type) { + try (DBHandler db = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId(); + HuaweiSampleProvider sampleProvider = new HuaweiSampleProvider(gbDevice, db.getDaoSession()); + + HuaweiActivitySample activitySample = new HuaweiActivitySample( + timestamp, + deviceId, + userId, + timestamp + duration, + FitnessData.MessageData.sleepId, + type, + 1, + ActivitySample.NOT_MEASURED, + ActivitySample.NOT_MEASURED, + ActivitySample.NOT_MEASURED, + ActivitySample.NOT_MEASURED, + ActivitySample.NOT_MEASURED + ); + activitySample.setProvider(sampleProvider); + + sampleProvider.addGBActivitySample(activitySample); + } catch (Exception e) { + LOG.error("Failed to add sleep activity to database", e); + } + } + + public void addStepData(int timestamp, short steps, short calories, short distance, byte spo, byte heartrate) { + try (DBHandler db = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId(); + HuaweiSampleProvider sampleProvider = new HuaweiSampleProvider(gbDevice, db.getDaoSession()); + + HuaweiActivitySample activitySample = new HuaweiActivitySample( + timestamp, + deviceId, + userId, + timestamp + 60, + FitnessData.MessageData.stepId, + ActivitySample.NOT_MEASURED, + 1, + steps, + calories, + distance, + spo, + heartrate + ); + activitySample.setProvider(sampleProvider); + + sampleProvider.addGBActivitySample(activitySample); + } catch (Exception e) { + LOG.error("Failed to add step data to database", e); + } + } + + public void addTotalFitnessData(int steps, int calories, int distance) { + LOG.debug("FITNESS total steps: " + steps); + LOG.debug("FITNESS total calories: " + calories); // TODO: May actually be kilocalories + LOG.debug("FITNESS total distance: " + distance + " m"); + + // TODO: potentially do more with this, maybe through realtime data? + } + + public Long addWorkoutTotalsData(Workout.WorkoutTotals.Response packet) { + try (DBHandler db = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId(); + + // Avoid duplicates + QueryBuilder qb = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where( + HuaweiWorkoutSummarySampleDao.Properties.UserId.eq(userId), + HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId), + HuaweiWorkoutSummarySampleDao.Properties.WorkoutNumber.eq(packet.number), + HuaweiWorkoutSummarySampleDao.Properties.StartTimestamp.eq(packet.startTime), + HuaweiWorkoutSummarySampleDao.Properties.EndTimestamp.eq(packet.endTime) + ); + List results = qb.build().list(); + Long workoutId = null; + if (!results.isEmpty()) + workoutId = results.get(0).getWorkoutId(); + + byte[] raw; + if (packet.rawData == null) + raw = null; + else + raw = StringUtils.bytesToHex(packet.rawData).getBytes(StandardCharsets.UTF_8); + + HuaweiWorkoutSummarySample summarySample = new HuaweiWorkoutSummarySample( + workoutId, + deviceId, + userId, + packet.number, + packet.status, + packet.startTime, + packet.endTime, + packet.calories, + packet.distance, + packet.stepCount, + packet.totalTime, + packet.duration, + packet.type, + packet.strokes, + packet.avgStrokeRate, + packet.poolLength, + packet.laps, + packet.avgSwolf, + raw + ); + db.getDaoSession().getHuaweiWorkoutSummarySampleDao().insertOrReplace(summarySample); + + return summarySample.getWorkoutId(); + } catch (Exception e) { + LOG.error("Failed to add workout totals data to database", e); + return null; + } + } + + public void addWorkoutSampleData(Long workoutId, List dataList) { + if (workoutId == null) + return; + + try (DBHandler db = GBApplication.acquireDB()) { + HuaweiWorkoutDataSampleDao dao = db.getDaoSession().getHuaweiWorkoutDataSampleDao(); + + for (Workout.WorkoutData.Response.Data data : dataList) { + byte[] unknown; + if (data.unknownData == null) + unknown = null; + else + unknown = StringUtils.bytesToHex(data.unknownData).getBytes(StandardCharsets.UTF_8); + + HuaweiWorkoutDataSample dataSample = new HuaweiWorkoutDataSample( + workoutId, + data.timestamp, + data.heartRate, + data.speed, + data.stepRate, + data.cadence, + data.stepLength, + data.groundContactTime, + data.impact, + data.swingAngle, + data.foreFootLanding, + data.midFootLanding, + data.backFootLanding, + data.eversionAngle, + data.swolf, + data.strokeRate, + unknown + ); + dao.insertOrReplace(dataSample); + } + } catch (Exception e) { + LOG.error("Failed to add workout data to database", e); + } + } + + public void addWorkoutPaceData(Long workoutId, List paceList) { + if (workoutId == null) + return; + + try (DBHandler db = GBApplication.acquireDB()) { + HuaweiWorkoutPaceSampleDao dao = db.getDaoSession().getHuaweiWorkoutPaceSampleDao(); + + for (Workout.WorkoutPace.Response.Block block : paceList) { + HuaweiWorkoutPaceSample paceSample = new HuaweiWorkoutPaceSample( + workoutId, + block.distance, + block.type, + block.pace, + block.correction + ); + dao.insertOrReplace(paceSample); + } + } catch (Exception e) { + LOG.error("Failed to add workout pace data to database", e); + } + } + + public void setWearLocation() { + try { + SetWearLocationRequest setWearLocationReq = new SetWearLocationRequest(this); + setWearLocationReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure Wear Location", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure Wear Location", e); + } + } + + public void getBatteryLevel() { + try { + GetBatteryLevelRequest batteryLevelReq = new GetBatteryLevelRequest(this); + batteryLevelReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to get battery Level", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to get battery Level", e); + } + } + + public void setActivateOnLift() { + try { + SetActivateOnLiftRequest setActivateOnLiftReq = new SetActivateOnLiftRequest(this); + setActivateOnLiftReq.doPerform(); + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac); + boolean statusDndLiftWrist = sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_LIFT_WRIST, false); + if (statusDndLiftWrist) { + setDnd(); + } + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure Activate on Rotate", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure Activate on Rotate", e); + } + } + + public void setNavigateOnRotate() { + try { + SetNavigateOnRotateRequest setNavigateOnRotateReq = new SetNavigateOnRotateRequest(this); + setNavigateOnRotateReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure Navigate on Rotate", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure Navigate on Rotate", e); + } + } + + public void setActivityReminder() { + try { + SetActivityReminderRequest setActivityReminderReq = new SetActivityReminderRequest(this); + setActivityReminderReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure Activity reminder", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure Activity reminder", e); + } + } + + public void setTrusleep() { + try { + SetTruSleepRequest setTruSleepReq = new SetTruSleepRequest(this); + setTruSleepReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to configure truSleep", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to configure truSleep", e); + } + } + + public void setDnd() { + try { + SendDndDeleteRequest sendDndDeleteReq = new SendDndDeleteRequest(this); + SendDndAddRequest sendDndAddReq = new SendDndAddRequest(this); + sendDndDeleteReq.nextRequest(sendDndAddReq); + sendDndDeleteReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to set DND", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to set DND", e); + } + } + + public void setDndNotWear() { + try { + SetWearMessagePushRequest setWearMessagePushReq = new SetWearMessagePushRequest(this); + setWearMessagePushReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Setting DND not wear failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Setting DND not wear failed", e); + } + + } + + private void setDisconnectNotification() { + try { + SetDisconnectNotification req = new SetDisconnectNotification(this); + req.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to set disconnect notification", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to set disconnect notification", e); + } + } + + private void setHeartrateAutomatic() { + try { + SetAutomaticHeartrateRequest req = new SetAutomaticHeartrateRequest(this); + req.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to set automatic heart rate", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to set automatic heart rate", e); + } + } + + private void setSpoAutomatic() { + try { + SetAutomaticSpoRequest req = new SetAutomaticSpoRequest(this); + req.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to set automatic SpO", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to set automatic SpO", e); + } + } + + public void sendDebugRequest() { + try { + LOG.debug("Send debug request"); + DebugRequest req = new DebugRequest(this); + req.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to send debug request", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to send debug request", e); + } + } + + public void onStopFindPhone() { + try { + LOG.debug("Send stop find phone request"); + StopFindPhoneRequest stopFindPhoneRequest = new StopFindPhoneRequest(this); + stopFindPhoneRequest.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to send stop find phone request", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to send stop find phone request", e); + } + } + + public void setLanguageSetting() { + try { + SetLanguageSettingRequest setLocaleReq = new SetLanguageSettingRequest(this); + setLocaleReq.doPerform(); + } catch (IOException e) { + // TODO: Use translatable string + GB.toast(context, "Failed to set language settings request", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to set language settings request", e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java new file mode 100644 index 000000000..28d2405c0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java @@ -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 . */ +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 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 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 qbSummary = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where( + HuaweiWorkoutSummarySampleDao.Properties.WorkoutId.eq(workoutId) + ); + List summarySamples = qbSummary.build().list(); + if (summarySamples.size() != 1) + return; + HuaweiWorkoutSummarySample summary = summarySamples.get(0); + + QueryBuilder qbData = db.getDaoSession().getHuaweiWorkoutDataSampleDao().queryBuilder().where( + HuaweiWorkoutDataSampleDao.Properties.WorkoutId.eq(workoutId) + ); + List dataSamples = qbData.build().list(); + + QueryBuilder 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 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 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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java new file mode 100644 index 000000000..789d7860d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java @@ -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 . */ +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 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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/AlarmsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/AlarmsRequest.java new file mode 100644 index 000000000..9e8174c92 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/AlarmsRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/DebugRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/DebugRequest.java new file mode 100644 index 000000000..acf87180a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/DebugRequest.java @@ -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 . */ +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 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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetActivityTypeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetActivityTypeRequest.java new file mode 100644 index 000000000..78aff25dc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetActivityTypeRequest.java @@ -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 . */ + +/* 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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetAuthRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetAuthRequest.java new file mode 100644 index 000000000..087dd8e3a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetAuthRequest.java @@ -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 . */ +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 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"); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java new file mode 100644 index 000000000..aff8f31d2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java @@ -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 . */ +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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondParamsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondParamsRequest.java new file mode 100644 index 000000000..2ea1489a4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondParamsRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondRequest.java new file mode 100644 index 000000000..edf8d8603 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBondRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetConnectStatusRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetConnectStatusRequest.java new file mode 100644 index 000000000..a51d026ad --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetConnectStatusRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDeviceStatusRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDeviceStatusRequest.java new file mode 100644 index 000000000..710da6894 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDeviceStatusRequest.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDndLiftWristTypeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDndLiftWristTypeRequest.java new file mode 100644 index 000000000..614f9df3a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetDndLiftWristTypeRequest.java @@ -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 . */ +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 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java new file mode 100644 index 000000000..1ea4c2332 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java @@ -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 . */ +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 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 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[]{})); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetExpandCapabilityRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetExpandCapabilityRequest.java new file mode 100644 index 000000000..98d21e750 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetExpandCapabilityRequest.java @@ -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 . */ + +/* 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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFitnessTotalsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFitnessTotalsRequest.java new file mode 100644 index 000000000..070803ce3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFitnessTotalsRequest.java @@ -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 . */ +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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetHiChainRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetHiChainRequest.java new file mode 100644 index 000000000..19ca04804 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetHiChainRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetLinkParamsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetLinkParamsRequest.java new file mode 100644 index 000000000..32871a9d6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetLinkParamsRequest.java @@ -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 . */ +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 +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationCapabilitiesRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationCapabilitiesRequest.java new file mode 100644 index 000000000..4f990ce82 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationCapabilitiesRequest.java @@ -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 . */ +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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationConstraintsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationConstraintsRequest.java new file mode 100644 index 000000000..b3399c928 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetNotificationConstraintsRequest.java @@ -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 . */ +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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPhoneInfoRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPhoneInfoRequest.java new file mode 100644 index 000000000..ad65d5cb5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPhoneInfoRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPincodeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPincodeRequest.java new file mode 100644 index 000000000..7b3004f91 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetPincodeRequest.java @@ -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 . */ +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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetProductInformationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetProductInformationRequest.java new file mode 100644 index 000000000..1c480d921 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetProductInformationRequest.java @@ -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 . */ +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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSecurityNegotiationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSecurityNegotiationRequest.java new file mode 100644 index 000000000..5dc11a33d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSecurityNegotiationRequest.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java new file mode 100644 index 000000000..1ceabd61b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataCountRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataCountRequest.java new file mode 100644 index 000000000..c9e7bb997 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataCountRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java new file mode 100644 index 000000000..729e6e5b5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java new file mode 100644 index 000000000..6ca4c6b0f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java @@ -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 . */ +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 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", + "" + ) + }); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataCountRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataCountRequest.java new file mode 100644 index 000000000..9a818a9fc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataCountRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataRequest.java new file mode 100644 index 000000000..d47609da6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetStepDataRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java new file mode 100644 index 000000000..94c205c76 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java @@ -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 . */ + +/* 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 commandsPerService; + private final List activatedServices; + + public GetSupportedCommandsRequest( + HuaweiSupportProvider support, + List activatedServices + ) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DeviceConfig.SupportedCommands.id; + this.commandsPerService = DeviceConfig.SupportedCommands.commandsPerService; + this.activatedServices = activatedServices; + } + + @Override + protected List 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(); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java new file mode 100644 index 000000000..55648fffe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java @@ -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 . */ +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 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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWearStatusRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWearStatusRequest.java new file mode 100644 index 000000000..986dc6931 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWearStatusRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutCountRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutCountRequest.java new file mode 100644 index 000000000..87559fb15 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutCountRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java new file mode 100644 index 000000000..13a88b372 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java @@ -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 . */ +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 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 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 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); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java new file mode 100644 index 000000000..d6b9a1900 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java @@ -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 . */ +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 remainder; + short number; + Long databaseId; + + public GetWorkoutPaceRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List 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 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); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java new file mode 100644 index 000000000..518ef270a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java @@ -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 . */ +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 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 remainder) { + super(support); + + this.serviceId = Workout.id; + this.commandId = Workout.WorkoutTotals.id; + + this.workoutNumbers = workoutNumbers; + this.remainder = remainder; + } + + @Override + protected List 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); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java new file mode 100644 index 000000000..65a8a2b37 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendAccountRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendAccountRequest.java new file mode 100644 index 000000000..9fd3be50c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendAccountRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndAddRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndAddRequest.java new file mode 100644 index 000000000..b27a4c4e1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndAddRequest.java @@ -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 . */ +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 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"); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndDeleteRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndDeleteRequest.java new file mode 100644 index 000000000..692147ddf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendDndDeleteRequest.java @@ -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 . */ +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 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"); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFactoryResetRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFactoryResetRequest.java new file mode 100644 index 000000000..aff94d018 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFactoryResetRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFitnessGoalRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFitnessGoalRequest.java new file mode 100644 index 000000000..7652f601e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFitnessGoalRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualCapabilityRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualCapabilityRequest.java new file mode 100644 index 000000000..d75412ced --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualCapabilityRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualModifyTimeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualModifyTimeRequest.java new file mode 100644 index 000000000..a87f777f9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendMenstrualModifyTimeRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotificationRequest.java new file mode 100644 index 000000000..e6588c304 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotificationRequest.java @@ -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 . */ +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 createRequest() throws RequestCreationException { + try { + return this.packet.serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Notification"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyHeartRateCapabilityRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyHeartRateCapabilityRequest.java new file mode 100644 index 000000000..b018aee59 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyHeartRateCapabilityRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyRestHeartRateCapabilityRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyRestHeartRateCapabilityRequest.java new file mode 100644 index 000000000..bb5363ab8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendNotifyRestHeartRateCapabilityRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java new file mode 100644 index 000000000..9b49ef3d1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivateOnLiftRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivateOnLiftRequest.java new file mode 100644 index 000000000..ba06249e5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivateOnLiftRequest.java @@ -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 . */ +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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivityReminderRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivityReminderRequest.java new file mode 100644 index 000000000..7cd289094 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetActivityReminderRequest.java @@ -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 . */ +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 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"); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticHeartrateRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticHeartrateRequest.java new file mode 100644 index 000000000..f83bc1259 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticHeartrateRequest.java @@ -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 . */ +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 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticSpoRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticSpoRequest.java new file mode 100644 index 000000000..2e27f192c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAutomaticSpoRequest.java @@ -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 . */ +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 SetAutomaticSpoRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetAutomaticSpoRequest.class); + + public SetAutomaticSpoRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = FitnessData.id; + this.commandId = FitnessData.EnableAutomaticSpo.id; + this.addToResponse = false; + } + + @Override + protected List createRequest() throws RequestCreationException { + boolean automaticSpoEnabled = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getBoolean(DeviceSettingsPreferenceConst.PREF_SPO_AUTOMATIC_ENABLE, false); + if (automaticSpoEnabled) + LOG.info("Attempting to enable automatic SpO"); + else + LOG.info("Attempting to disable automatic SpO"); + try { + return new FitnessData.EnableAutomaticSpo.Request(paramsProvider, automaticSpoEnabled).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDateFormatRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDateFormatRequest.java new file mode 100644 index 000000000..cbf334df3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDateFormatRequest.java @@ -0,0 +1,79 @@ +/* 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 . */ +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.devices.huawei.packets.DeviceConfig.DateFormat; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetDateFormatRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetDateFormatRequest.class); + + public SetDateFormatRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DateFormat.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + int time = DeviceConfig.Time.hours12; + int date; + String timeFormat = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, "auto"); + if (timeFormat.equals("auto")) { + if (android.text.format.DateFormat.is24HourFormat(GBApplication.getContext())) + time = DeviceConfig.Time.hours24; + } else if (timeFormat.equals("24h")) { + time = DeviceConfig.Time.hours24; + } + String dateFormat = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getString(DeviceSettingsPreferenceConst.PREF_DATEFORMAT, "MM/dd/yyyy"); + switch (dateFormat) { + case "MM/dd/yyyy": + date = DeviceConfig.Date.monthFirst; + break; + case "dd.MM.yyyy": + case "dd/MM/yyyy": + date = DeviceConfig.Date.dayFirst; + break; + default: + date = DeviceConfig.Date.yearFirst; + } + try { + return new DateFormat.Request(paramsProvider, (byte) date, (byte) time).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Date Format"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDisconnectNotification.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDisconnectNotification.java new file mode 100644 index 000000000..f8f2ace98 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetDisconnectNotification.java @@ -0,0 +1,60 @@ +/* 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 . */ +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.packets.DisconnectNotification; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetDisconnectNotification extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetDisconnectNotification.class); + + public SetDisconnectNotification(HuaweiSupportProvider support) { + super(support); + this.serviceId = DisconnectNotification.id; + this.commandId = DisconnectNotification.DisconnectNotificationSetting.id; + this.addToResponse = false; + } + + @Override + protected List createRequest() throws RequestCreationException { + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(supportProvider.getDeviceMac()); + boolean notificationEnable = sharedPrefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED, true); + if (notificationEnable) { + LOG.info("Attempting to enable disconnect notification"); + } else { + LOG.info("Attempting to disable disconnect notification"); + } + try { + return new DisconnectNotification.DisconnectNotificationSetting.Request( + paramsProvider, + notificationEnable + ).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetLanguageSettingRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetLanguageSettingRequest.java new file mode 100644 index 000000000..889d8d61a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetLanguageSettingRequest.java @@ -0,0 +1,77 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.LocaleConfig; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.LocaleConfig.SetLanguageSetting; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetLanguageSettingRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetLanguageSettingRequest.class); + + public SetLanguageSettingRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = LocaleConfig.id; + this.commandId = SetLanguageSetting.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + String localeString = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getString(DeviceSettingsPreferenceConst.PREF_LANGUAGE, "auto"); + if (localeString == null || localeString.equals("auto")) { + String language = Locale.getDefault().getLanguage(); + String country = Locale.getDefault().getCountry(); + if (country.equals("")) { + country = language; + } + localeString = language + "-" + country.toUpperCase(); + } else { + localeString = localeString.replace("_", "-"); + } + LOG.debug("localeString: " + localeString); + String measurementString = GBApplication + .getPrefs() + .getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, getContext().getString(R.string.p_unit_metric)); + LOG.debug("measurementString: " + measurementString); + byte measurement = measurementString.equals("metric") ? LocaleConfig.MeasurementSystem.metric : LocaleConfig.MeasurementSystem.imperial; + try { + return new SetLanguageSetting(paramsProvider, localeString.getBytes(StandardCharsets.UTF_8), measurement).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Locale"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMediumToStrengthThresholdRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMediumToStrengthThresholdRequest.java new file mode 100644 index 000000000..530f0c586 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMediumToStrengthThresholdRequest.java @@ -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 . */ +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.MediumToStrengthThreshold; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetMediumToStrengthThresholdRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetMediumToStrengthThresholdRequest.class); + + public SetMediumToStrengthThresholdRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = FitnessData.id; + this.commandId = MediumToStrengthThreshold.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + //Hardcoded value till interface enable threshold values + return new MediumToStrengthThreshold.Request(paramsProvider, + (byte)0x6E, + (byte)0x3C, + (byte)0x05, + (byte)0x40, + (byte)0x50, + (byte)0x03 + ).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Activate On Rotate"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicRequest.java new file mode 100644 index 000000000..956ddfe6f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicRequest.java @@ -0,0 +1,104 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests; + +import android.content.Context; +import android.media.AudioManager; +import android.media.session.PlaybackState; + +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.MusicControl; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetMusicRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetMusicRequest.class); + + private final MusicStateSpec musicStateSpec; + private final MusicSpec musicSpec; + + public SetMusicRequest(HuaweiSupportProvider support, MusicStateSpec musicStateSpec, MusicSpec musicSpec) { + super(support); + this.serviceId = MusicControl.id; + this.commandId = MusicControl.MusicInfo.id; + this.musicStateSpec = musicStateSpec; + this.musicSpec = musicSpec; + } + + private byte convertMusicState(int in) { + switch (in) { + case MusicStateSpec.STATE_PLAYING: + return PlaybackState.STATE_PLAYING; + case MusicStateSpec.STATE_PAUSED: + return PlaybackState.STATE_PAUSED; + case MusicStateSpec.STATE_STOPPED: + return PlaybackState.STATE_STOPPED; + case MusicStateSpec.STATE_UNKNOWN: + default: + return PlaybackState.STATE_NONE; + } + } + + @Override + protected List createRequest() throws RequestCreationException { + String artistName = ""; + String songName = ""; + byte playState = convertMusicState(MusicStateSpec.STATE_UNKNOWN); + if (this.musicSpec != null) { + artistName = this.musicSpec.artist; + songName = this.musicSpec.track; + } + if (this.musicStateSpec != null) + playState = convertMusicState(this.musicStateSpec.state); + AudioManager audioManager = (AudioManager) this.supportProvider.getContext().getSystemService(Context.AUDIO_SERVICE); + byte maxVolume = (byte) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + byte currentVolume = (byte) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + + try { + return new MusicControl.MusicInfo.Request( + paramsProvider, + artistName, + songName, + playState, + maxVolume, + currentVolume + ).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + if (receivedPacket instanceof MusicControl.MusicInfo.Response) { + if (((MusicControl.MusicInfo.Response) receivedPacket).ok) { + LOG.debug("Music information acknowledged by band"); + } else { + LOG.warn(((MusicControl.MusicInfo.Response) receivedPacket).error); + } + } else { + LOG.error("MusicInfo response is not of type MusicInfo response"); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicStatusRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicStatusRequest.java new file mode 100644 index 000000000..ded5754b4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetMusicStatusRequest.java @@ -0,0 +1,45 @@ +/* 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 . */ +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.MusicControl; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetMusicStatusRequest extends Request { + private final int returnValue; + + public SetMusicStatusRequest(HuaweiSupportProvider support, byte commandId, int returnValue) { + super(support); + this.serviceId = MusicControl.id; + this.commandId = commandId; + this.returnValue = returnValue; + this.addToResponse = false; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new MusicControl.MusicStatusRequest(paramsProvider, (byte) commandId, returnValue).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNavigateOnRotateRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNavigateOnRotateRequest.java new file mode 100644 index 000000000..9e8dd12af --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNavigateOnRotateRequest.java @@ -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 . */ +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.DeviceConfig; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetNavigateOnRotateRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetNavigateOnRotateRequest.class); + + public SetNavigateOnRotateRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DeviceConfig.NavigateOnRotateRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + boolean navigate = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getBoolean(MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO, false); + try { + return new DeviceConfig.NavigateOnRotateRequest(paramsProvider, navigate).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Navigate On Rotate"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNotificationRequest.java new file mode 100644 index 000000000..90d57c344 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetNotificationRequest.java @@ -0,0 +1,56 @@ +/* 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 . */ +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.service.devices.huawei.HuaweiSupportProvider; + +public class SetNotificationRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetNotificationRequest.class); + + public SetNotificationRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = Notifications.id; + this.commandId = Notifications.NotificationStateRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + boolean status = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false); + try { + return new Notifications.NotificationStateRequest(paramsProvider, status).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Notification"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeRequest.java new file mode 100644 index 000000000..6c3ebb910 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeRequest.java @@ -0,0 +1,51 @@ +/* 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 . */ +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 SetTimeRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetTimeRequest.class); + + public SetTimeRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DeviceConfig.TimeRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new DeviceConfig.TimeRequest(paramsProvider).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Time"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeZoneIdRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeZoneIdRequest.java new file mode 100644 index 000000000..f6cda5a6e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTimeZoneIdRequest.java @@ -0,0 +1,51 @@ +/* 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 . */ +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 SetTimeZoneIdRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetTimeZoneIdRequest.class); + + public SetTimeZoneIdRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DeviceConfig.TimeZoneIdRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new DeviceConfig.TimeZoneIdRequest(paramsProvider).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Time ZoneId"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java new file mode 100644 index 000000000..a029fcb1d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java @@ -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 . */ +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.HuaweiConstants; +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 SetTruSleepRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetTruSleepRequest.class); + + public SetTruSleepRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = FitnessData.id; + this.commandId = FitnessData.TruSleep.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + boolean truSleepSwitch = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getBoolean(HuaweiConstants.PREF_HUAWEI_TRUSLEEP, false); + try { + return new FitnessData.TruSleep.Request(paramsProvider, truSleepSwitch).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set TruSleep"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearLocationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearLocationRequest.java new file mode 100644 index 000000000..c5cfe3f42 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearLocationRequest.java @@ -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 . */ +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 SetWearLocationRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetWearLocationRequest.class); + + public SetWearLocationRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DeviceConfig.WearLocationRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + String locationString = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left"); + byte location = (byte) (locationString.equals("left") ? 1 : 0); + try { + return new DeviceConfig.WearLocationRequest(paramsProvider, location).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set Wear Location"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearMessagePushRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearMessagePushRequest.java new file mode 100644 index 000000000..e7d202d75 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWearMessagePushRequest.java @@ -0,0 +1,56 @@ +/* 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 . */ +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.service.devices.huawei.HuaweiSupportProvider; + +public class SetWearMessagePushRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetWearMessagePushRequest.class); + + public SetWearMessagePushRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = Notifications.id; + this.commandId = Notifications.WearMessagePushRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + boolean status = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getBoolean(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOT_WEAR, false); + try { + return new Notifications.WearMessagePushRequest(paramsProvider, status).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set WearMessage Push "); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWorkModeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWorkModeRequest.java new file mode 100644 index 000000000..c7e48192f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetWorkModeRequest.java @@ -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 . */ +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.HuaweiConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.WorkMode; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SetWorkModeRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SetWorkModeRequest.class); + + public SetWorkModeRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = WorkMode.id; + this.commandId = WorkMode.SwitchStatusRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + String workModeString = GBApplication + .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) + .getString(HuaweiConstants.PREF_HUAWEI_WORKMODE, "auto"); + boolean workMode = workModeString.equals("auto"); + try { + return new WorkMode.SwitchStatusRequest(paramsProvider, workMode).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() { + LOG.debug("handle Set WorkMode"); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopFindPhoneRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopFindPhoneRequest.java new file mode 100644 index 000000000..a5581f9ca --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopFindPhoneRequest.java @@ -0,0 +1,40 @@ +/* Copyright (C) 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 . */ +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.FindPhone; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class StopFindPhoneRequest extends Request { + public StopFindPhoneRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = FindPhone.id; + this.commandId = FindPhone.StopRequest.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new FindPhone.StopRequest(paramsProvider).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopNotificationRequest.java new file mode 100644 index 000000000..69017d802 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/StopNotificationRequest.java @@ -0,0 +1,53 @@ +/* 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 . */ +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.Notifications; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class StopNotificationRequest extends Request { + public StopNotificationRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = Notifications.id; + this.commandId = Notifications.NotificationActionRequest.id; + this.addToResponse = false; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new Notifications.NotificationActionRequest( + paramsProvider, + supportProvider.getNotificationId(), + Notifications.NotificationType.stopNotification, + Notifications.TextEncoding.standard, + null, + Notifications.TextEncoding.standard, + null, + Notifications.TextEncoding.standard, + null, + null + ).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java index a841ac640..71bd155c8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java @@ -2,13 +2,21 @@ package nodomain.freeyourgadget.gadgetbridge.util; import android.annotation.SuppressLint; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class CryptoUtils { @@ -25,4 +33,85 @@ public class CryptoUtils { ecipher.init(Cipher.DECRYPT_MODE, newKey); return ecipher.doFinal(value); } + + public static byte[] encryptAES_CBC_Pad(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, paramSpec); + return cipher.doFinal(data); + } + + public static byte[] decryptAES_CBC_Pad(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, paramSpec); + return cipher.doFinal(data); + } + + public static byte[] encryptAES_GCM_NoPad(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + GCMParameterSpec paramSpec = new GCMParameterSpec(16 * 8, iv); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, paramSpec); + if (aad != null) { + cipher.updateAAD(aad); + } + return cipher.doFinal(data); + } + + public static byte[] decryptAES_GCM_NoPad(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + GCMParameterSpec paramSpec = new GCMParameterSpec(16 * 8, iv); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, paramSpec); + if (aad != null) { + cipher.updateAAD(aad); + } + return cipher.doFinal(data); + } + + public static byte[] calcHmacSha256(byte[] secretKey, byte[] message) throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "HmacSHA256"); + mac.init(secretKeySpec); + return mac.doFinal(message); + } + + public static byte[] digest(byte[] message) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(message); + } + + // Thanks to https://www.javatips.net/api/keywhiz-master/hkdf/src/main/java/keywhiz/hkdf/Hkdf.java for light code + public static byte[] hkdfSha256(byte[] secretKey, byte[] salt, byte[] info, int outputLength) throws InvalidKeyException, NoSuchAlgorithmException { // return 32 byte len session key - outputLength=32 ? + //extract start + byte[] pseudoRandomKey = calcHmacSha256(salt, secretKey); + SecretKey pseudoSecretKey = new SecretKeySpec(pseudoRandomKey, "HmacSHA256"); + //extract end + int hashLen = 32; + int n = (outputLength % hashLen == 0) ? outputLength / hashLen : (outputLength / hashLen) + 1; + byte[] hashRound = new byte[0]; + + ByteBuffer generatedBytes = ByteBuffer.allocate(Math.multiplyExact(n, hashLen)); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(pseudoSecretKey); + for (int roundNum = 1; roundNum <= n; roundNum++) { + mac.reset(); + ByteBuffer t = ByteBuffer.allocate(hashRound.length + info.length + 1); + t.put(hashRound); + t.put(info); + t.put((byte)roundNum); + hashRound = mac.doFinal(t.array()); + generatedBytes.put(hashRound); + } + + byte[] result = new byte[outputLength]; + generatedBytes.rewind(); + generatedBytes.get(result, 0, outputLength); + return result; + + + } } diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index c57150f86..ccc4113ea 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -297,6 +297,11 @@ @string/mi2_dnd_automatic @string/mi2_dnd_scheduled + + @string/mi2_dnd_off + @string/dnd_all_day + @string/mi2_dnd_scheduled + @string/p_off @string/p_automatic @@ -3186,6 +3191,16 @@ net.osmand.dev + + @string/automatic + @string/manual + + + + auto + manual + + @string/arabic @string/bengali diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7e09d949..bf0e0bfdd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -941,6 +941,7 @@ Time Disconnect notification + Notification on device when disconnected from BT. Button actions Specify button press actions Button press count @@ -986,6 +987,8 @@ Start time End time Activate display upon lift during Do Not Disturb + Only if activate display upon lift enabled + Do not disturb when not wearing Band screen unlock" Swipe up to unlock the band\'s screen Night mode @@ -1009,6 +1012,7 @@ The band will vibrate if your phone disconnects from the band Interface language Automatic + Manual Simplified Chinese Traditional Chinese English @@ -1206,6 +1210,7 @@ At sunset Automatic (sleep detection) Scheduled (time interval) + All day Duration Attempting to pair with %1$s Bonding with %1$s failed immediately. @@ -1423,6 +1428,21 @@ Sony WF-1000XM5 Sony LinkBuds S Binary sensor + Honor Band 3 + Honor Band 4 + Honor Band 5 + Honor Band 6 + Honor Band 7 + Huawei Band (AW70) + Huawei Band 6 + Huawei Band 7 + Huawei Band 8 + Huawei Watch GT + Huawei Band 4 (Pro) + Huawei Watch GT 2 (Pro) + Huawei Watch GT 2e + Huawei Talk Band B6 + Huawei Watch GT 3 (Pro) Femometer Vinca II Xiaomi Watch Lite Redmi Watch 3 Active @@ -2203,6 +2223,21 @@ ###ft + Work Mode + Do not uncheck smart wakeup checkbox. + Do not check smart wakeup checkbox. + HUAWEI TruSleep ™ + Monitor your sleep quality and breathing pattern in real time.\nAnalyze your sleep patterns and accurately diagnose 6 types of sleeping problems. + Improved sleep monitoring + Activity recognition settings + recognize running + recognize biking + recognize walking + recognize rowing + none + ask + auto + Menu Some buttons cannot be configured because their functions are hard-coded in the watch firmware.\n\nWarning: long-pressing the upper button when a watchface from the official Fossil app is installed will also toggle between showing/hiding widgets. OpenTracks package name @@ -2212,6 +2247,31 @@ pre-setting position to %s Light up on new notification + Enable accepting calls + Enable accepting calls from the device + Enable rejecting calls + Enable rejecting calls from the device + + Disable find my phone when do not disturb is active + + Enable automatic heartrate measuring + Enable automatic SpO2 measuring + + Force options + Some devices falsely claim not to have support for some options. This settings can be used to enable them anyway.\nUSE AT YOUR OWN RISK\nRead the wiki + Force smart alarm + Force smart alarms support.\nUSE AT YOUR OWN RISK + Force wear location + Force wear location support.\nUSE AT YOUR OWN RISK + Force Do Not Disturb support + Force Do Not Disturb support.\nUSE AT YOUR OWN RISK + Ignore wakeup start status + May help with proper sleep detection. Visible immediately in the daily activities view. + Ignore wakeup end status + May help with proper sleep detection. Visible immediately in the daily activities view. + "Reparse workout data" + "This will only do something after certain updates" + no devices connected %d devices connected Set parent folder @@ -2266,6 +2326,8 @@ Turbo Speed Lights Blinking + Send a debug request to Huawei device + Debug request AsteroidOS SoFlow SO6 Lock diff --git a/app/src/main/res/xml/devicesettings_allow_accept_reject_calls.xml b/app/src/main/res/xml/devicesettings_allow_accept_reject_calls.xml new file mode 100644 index 000000000..f233e9471 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_allow_accept_reject_calls.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_disable_find_phone_with_dnd.xml b/app/src/main/res/xml/devicesettings_disable_find_phone_with_dnd.xml new file mode 100644 index 000000000..6dc02369b --- /dev/null +++ b/app/src/main/res/xml/devicesettings_disable_find_phone_with_dnd.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_donotdisturb_allday_liftwirst.xml b/app/src/main/res/xml/devicesettings_donotdisturb_allday_liftwirst.xml new file mode 100644 index 000000000..1109a700c --- /dev/null +++ b/app/src/main/res/xml/devicesettings_donotdisturb_allday_liftwirst.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_force_options.xml b/app/src/main/res/xml/devicesettings_force_options.xml new file mode 100644 index 000000000..8b1e3ba89 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_force_options.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_heartrate_automatic_enable.xml b/app/src/main/res/xml/devicesettings_heartrate_automatic_enable.xml new file mode 100644 index 000000000..8bed9f4a5 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_heartrate_automatic_enable.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/xml/devicesettings_huawei.xml b/app/src/main/res/xml/devicesettings_huawei.xml new file mode 100644 index 000000000..d0c8a8011 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_huawei.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_huawei_debug.xml b/app/src/main/res/xml/devicesettings_huawei_debug.xml new file mode 100644 index 000000000..b02d20acc --- /dev/null +++ b/app/src/main/res/xml/devicesettings_huawei_debug.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/xml/devicesettings_huawei_reparse_workout_data.xml b/app/src/main/res/xml/devicesettings_huawei_reparse_workout_data.xml new file mode 100644 index 000000000..9e1226c25 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_huawei_reparse_workout_data.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_inactivity_sheduled.xml b/app/src/main/res/xml/devicesettings_inactivity_sheduled.xml new file mode 100644 index 000000000..bc98c8ad8 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_inactivity_sheduled.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_spo_automatic_enable.xml b/app/src/main/res/xml/devicesettings_spo_automatic_enable.xml new file mode 100644 index 000000000..f59cd0705 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_spo_automatic_enable.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/xml/devicesettings_trusleep.xml b/app/src/main/res/xml/devicesettings_trusleep.xml new file mode 100644 index 000000000..7a65510a3 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_trusleep.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_workmode.xml b/app/src/main/res/xml/devicesettings_workmode.xml new file mode 100644 index 000000000..51edc5dbf --- /dev/null +++ b/app/src/main/res/xml/devicesettings_workmode.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiCrypto.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiCrypto.java new file mode 100644 index 000000000..ccb60eae6 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiCrypto.java @@ -0,0 +1,77 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class TestHuaweiCrypto { + + private void printByteArrayAsHex(String name, byte[] array) { + System.out.print(name + ": ["); + for (int i = 0; i < array.length - 1; i++) { + System.out.print(Integer.toHexString(array[i] & 0xFF) + ", "); + } + System.out.println(Integer.toHexString(array[array.length - 1] & 0xFF) + "]"); + } + + private void printIntArray(String name, int[] array) { + System.out.print(name + ": ["); + for (int i = 0; i < array.length - 1; i++) { + System.out.print(array[i] + ", "); + } + System.out.println(array[array.length - 1] + "]"); + } + + @Test + public void testGenerateNonce() { + // The function output contains randomness, so we test multiple times + + // We also check how often each byte is present, and that the difference isn't too high + // Note that this may fail due to the randomness + int[] bytePresent = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + Assert.assertEquals(256, bytePresent.length); + + for (int i = 0; i < 1000; i++) { + byte[] output = HuaweiCrypto.generateNonce(); + + printByteArrayAsHex("Output", output); + + Assert.assertEquals(16, output.length); + + for (byte b : output) { + bytePresent[b & 0xFF] += 1; + } + } + + printIntArray("Bytes present", bytePresent); + + int minCount = Integer.MAX_VALUE; + int maxCount = Integer.MIN_VALUE; + for (int c : bytePresent) { + minCount = Math.min(c, minCount); + maxCount = Math.max(c, maxCount); + } + + // The limit here is quite arbitrary, erring on the high side + if (maxCount - minCount > 60) { + Assert.fail("Difference in byte counts is suspiciously high, check the randomness of the nonce."); + } + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiPacket.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiPacket.java new file mode 100644 index 000000000..6fa7efce0 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiPacket.java @@ -0,0 +1,269 @@ +/* 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 . */ + +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone; + +public class TestHuaweiPacket { + + HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider() { + + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return false; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + HuaweiPacket.ParamsProvider paramsProviderEncrypt = new HuaweiPacket.ParamsProvider() { + + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + HuaweiPacket.ParamsProvider paramsProviderSmallSlice = new HuaweiPacket.ParamsProvider() { + + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return false; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0x10; + } + }; + + @Test + public void testEmptyPacketParse() { + byte[] input = {}; + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + + try { + packet.parse(input); + } catch (HuaweiPacket.LengthMismatchException e) { + // Pass + } catch (HuaweiPacket.ParseException e) { + Assert.fail(); + } + } + + @Test + public void testUnknownUnencryptedPacketParse() throws HuaweiPacket.ParseException { + byte[] input = {0x5a, 0x00, 0x07, 0x00, 0x7f, 0x7f, 0x01, 0x02, 0x03, 0x04, 0x40, (byte) 0xb6}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, (short) 0x0304); + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet = packet.parse(input); + + Assert.assertEquals(HuaweiPacket.class, packet.getClass()); + Assert.assertEquals(0x7f, packet.serviceId); + Assert.assertEquals(0x7f, packet.commandId); + Assert.assertFalse(packet.isEncrypted); + Assert.assertTrue(packet.complete); + Assert.assertEquals(expectedTlv, packet.getTlv()); + } + + @Test + public void testUnknownEncryptedPacketParse() throws HuaweiPacket.ParseException { + byte[] input = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x7f, (byte) 0x7f, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x9e, (byte) 0x40, (byte) 0xe1, (byte) 0xea, (byte) 0x15, (byte) 0xf6, (byte) 0x50, (byte) 0x80, (byte) 0x8c, (byte) 0x45, (byte) 0x19, (byte) 0xd5, (byte) 0x2a, (byte) 0xbb, (byte) 0x29, (byte) 0xb8, (byte) 0xD5, (byte) 0x24}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, (short) 0x0304); + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet = packet.parse(input); + + Assert.assertEquals(HuaweiPacket.class, packet.getClass()); + Assert.assertEquals(0x7f, packet.serviceId); + Assert.assertEquals(0x7f, packet.commandId); + Assert.assertTrue(packet.isEncrypted); + Assert.assertTrue(packet.complete); + Assert.assertEquals(expectedTlv, packet.getTlv()); + } + + @Test + public void testKnownEncryptedPacketParse() throws HuaweiPacket.ParseException { + byte[] input = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0xf4, (byte) 0xf4}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, false); + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet = packet.parse(input); + + Assert.assertEquals(FindPhone.Response.class, packet.getClass()); + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertTrue(packet.isEncrypted); + Assert.assertTrue(packet.complete); + Assert.assertEquals(expectedTlv, packet.getTlv()); + } + + @Test + public void testUnencryptedUnslicedSerialize() throws HuaweiPacket.CryptoException { + byte serviceId = 0x01; + byte commandId = 0x02; + HuaweiTLV tlv = new HuaweiTLV() + .put(0x03, 0x05060708); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0xA4, (byte) 0xF0}; + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet.isSliced = false; + packet.isEncrypted = false; + packet.serviceId = serviceId; + packet.commandId = commandId; + packet.setTlv(tlv); + + List output = packet.serialize(); + + Assert.assertEquals(1, output.size()); + Assert.assertArrayEquals(expected, output.get(0)); + } + + @Test + public void testEncryptedUnslicedSerialize() throws HuaweiPacket.CryptoException { + byte serviceId = 0x01; + byte commandId = 0x02; + HuaweiTLV tlv = new HuaweiTLV() + .put(0x03, 0x05060708); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x3b, (byte) 0x89, (byte) 0xfc, (byte) 0x79, (byte) 0xd9, (byte) 0x05, (byte) 0x5e, (byte) 0xed, (byte) 0x52, (byte) 0x35, (byte) 0xfe, (byte) 0x16, (byte) 0xa0, (byte) 0x8a, (byte) 0x4d, (byte) 0x53, (byte) 0x93, (byte) 0xc7}; + + HuaweiPacket packet = new HuaweiPacket(paramsProviderEncrypt); + packet.isSliced = false; + packet.isEncrypted = true; + packet.serviceId = serviceId; + packet.commandId = commandId; + packet.setTlv(tlv); + + List output = packet.serialize(); + + Assert.assertEquals(1, output.size()); + Assert.assertArrayEquals(expected, output.get(0)); + } + + @Test + public void testUnencryptedSlicedSerialize() throws HuaweiPacket.CryptoException { + byte serviceId = 0x01; + byte commandId = 0x02; + HuaweiTLV tlv = new HuaweiTLV() + .put(0x01, 0x00) + .put(0x02, 0x00) + .put(0x03, 0x00) + .put(0x04, 0x00); + + byte[] expected1 = {(byte) 0x5a, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x01, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0xcc, (byte) 0x98}; + byte[] expected2 = {(byte) 0x5a, (byte) 0x00, (byte) 0x0b, (byte) 0x02, (byte) 0x01, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0xfa, (byte) 0xd3}; + byte[] expected3 = {(byte) 0x5a, (byte) 0x00, (byte) 0x0a, (byte) 0x03, (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x37, (byte) 0xca}; + + HuaweiPacket packet = new HuaweiPacket(paramsProviderSmallSlice); + packet.isSliced = true; + packet.isEncrypted = false; + packet.serviceId = serviceId; + packet.commandId = commandId; + packet.setTlv(tlv); + + List output = packet.serialize(); + + Assert.assertEquals(3, output.size()); + Assert.assertArrayEquals(expected1, output.get(0)); + Assert.assertArrayEquals(expected2, output.get(1)); + Assert.assertArrayEquals(expected3, output.get(2)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTLV.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTLV.java new file mode 100644 index 000000000..ed0004071 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTLV.java @@ -0,0 +1,532 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +public class TestHuaweiTLV { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return false; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testSerializeEmpty() { + byte[] expectedOutput = {}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.serialize()); + } + + @Test + public void testSerialize() { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x42})); + byte[] expectedOutput = {0x01, 0x00, 0x01, 0x01, 0x42}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.serialize()); + } + + @Test + public void testPutEmptyTag() { + int tag = 0x01; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV().put(tag); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutNullByteArray() { + int tag = 0x01; + byte[] input = null; + ArrayList expectedValueMap = new ArrayList<>(); + + //noinspection ConstantConditions + HuaweiTLV huaweiTLV = new HuaweiTLV().put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutByteArray() { + int tag = 0x01; + byte[] input = {0x01, 0x02, 0x03}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02, 0x03})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutByte() { + int tag = 0x01; + byte input = 0x13; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x13})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutBooleans() { + int tag1 = 0x01; + int tag2 = 0x02; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01})); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {0x00})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag1, true) + .put(tag2, false); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutInt() { + int tag = 0x01; + int input = 0xDEADBEEF; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutShort() { + int tag = 0x01; + short input = (short) 0xCAFE; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutString() { + int tag = 0x01; + String input = "Hello world!"; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV( + (byte) 0x01, + new byte[] {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21} + )); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutHuaweiTLV() { + int tag = 0x01; + HuaweiTLV input = new HuaweiTLV().put(0x01, (short) 0x1337); + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02, 0x13, 0x37})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutMultipleEqualEmptyTags() { + int tag = 0x01; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag) + .put(tag); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseEmpty() { + byte[] input = {}; + ArrayList expectedValueMap = new ArrayList<>(); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseSingleByte() { + byte[] input = {0x01, 0x01, 0x01}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseBytes() { + byte[] input = {0x01, 0x04, (byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseZeroOffsetLength() { + byte[] input = {}; + ArrayList expectedValueMap = new ArrayList<>(); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input, 0, 0); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testParseMalformed() { + byte[] input = {(byte) 0x01, (byte) 0x01}; + new HuaweiTLV() + .parse(input); + Assert.fail(); + } + + @Test + public void testParseOffsetLength() { + byte[] input = {(byte) 0x90, (byte) 0x90, (byte) 0x90, 0x01, 0x00}; + int offset = 3; + int length = 2; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input, offset, length); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testParseWrongOffsetLength() { + byte[] input = {}; + new HuaweiTLV() + .parse(input, 1, 1); + Assert.fail(); + } + + @Test + public void testGetBytesEmpty() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + byte[] expectedOutput = new byte[] {}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.getBytes(tag)); + } + + @Test + public void testGetBytes() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02, 0x03})); + byte[] expectedOutput = new byte[] {0x01, 0x02, 0x03}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.getBytes(tag)); + } + + @Test + public void testGetByte() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x04})); + Byte expectedOutput = 0x04; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getByte(tag)); + } + + @Test + public void testGetBooleans() { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01})); + input.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {0x00})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(true, huaweiTLV.getBoolean(0x01)); + Assert.assertEquals(false, huaweiTLV.getBoolean(0x02)); + } + + @Test + public void testGetInteger() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})); + Integer expectedOutput = 0xDEADBEEF; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getInteger(tag)); + } + + @Test + public void testGetShort() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + Short expectedOutput = (short) 0xCAFE; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getShort(tag)); + } + + @Test + public void testGetString() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21})); + String expectedOutput = "Hello world!"; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getString(tag)); + } + + @Test + public void testGetObject() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x00})); + HuaweiTLV expectedOutput = new HuaweiTLV().put(0x01); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + // assertEquals currently tests if the objects are the same, thus this would fail + // Assert.assertEquals(expectedOutput, huaweiTLV.getObject(tag)); + + HuaweiTLV result = huaweiTLV.getObject(tag); + + Assert.assertEquals(expectedOutput.valueMap, result.valueMap); + } + + @Test + public void testContains() { + int existingTag = 0x01; + int nonExistingTag = 0x02; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertTrue(huaweiTLV.contains(existingTag)); + Assert.assertFalse(huaweiTLV.contains(nonExistingTag)); + } + + @Test + public void testRemoveExisting() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x13, 0x37})); + input.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {})); + byte[] expectedOutput = {0x13, 0x37}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.remove(tag)); + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testRemoveNonExisting() { + int tag = 0x02; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x13, 0x37})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertNull(huaweiTLV.remove(tag)); + Assert.assertEquals(input, huaweiTLV.valueMap); + } + + @Test + public void testRemoveDouble() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA})); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xFE})); + byte[] expectedOutput1 = {(byte) 0xFE}; + byte[] expectedOutput2 = {(byte) 0xCA}; + ArrayList expectedValueMap1 = new ArrayList<>(); + expectedValueMap1.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA})); + ArrayList expectedValueMap2 = new ArrayList<>(); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput1, huaweiTLV.remove(tag)); + Assert.assertEquals(expectedValueMap1, huaweiTLV.valueMap); + Assert.assertArrayEquals(expectedOutput2, huaweiTLV.remove(tag)); + Assert.assertEquals(expectedValueMap2, huaweiTLV.valueMap); + } + + @Test + public void testToStringEmpty() { + ArrayList input = new ArrayList<>(); + String expectedOutput = "Empty"; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.toString()); + } + + @Test + public void testToString() { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02})); + input.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {0x03, 0x04})); + String expectedOutput = "{tag: 1 - Value: 0102} - {tag: 2 - Value: 0304}"; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.toString()); + } + + /** + * Following test also depends on the HuaweiCrypto class functioning correctly + */ + @Test + public void testEncrypt() throws HuaweiCrypto.CryptoException { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + + byte[] expectedCiphertext = {(byte) 0x0E, (byte) 0xA0, (byte) 0x01, (byte) 0xBB, (byte) 0x1E, (byte) 0xDA, (byte) 0xCB, (byte) 0x09, (byte) 0x83, (byte) 0x20, (byte) 0x40, (byte) 0x7D, (byte) 0x97, (byte) 0x1B, (byte) 0xF6, (byte) 0xD0}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x7C, new byte[] {0x01})); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x7D, secretsProvider.getIv())); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x7E, expectedCiphertext)); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + HuaweiTLV encryptedTlv = huaweiTLV.encrypt(secretsProvider); + + Assert.assertEquals(input, huaweiTLV.valueMap); + Assert.assertEquals(expectedValueMap, encryptedTlv.valueMap); + } + + /** + * Following test also depends on the HuaweiCrypto class functioning correctly + */ + @Test + public void testDecrypt() throws HuaweiCrypto.CryptoException { + byte[] ciphertext = {(byte) 0x0E, (byte) 0xA0, (byte) 0x01, (byte) 0xBB, (byte) 0x1E, (byte) 0xDA, (byte) 0xCB, (byte) 0x09, (byte) 0x83, (byte) 0x20, (byte) 0x40, (byte) 0x7D, (byte) 0x97, (byte) 0x1B, (byte) 0xF6, (byte) 0xD0}; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x7C, new byte[] {0x01})); + input.add(new HuaweiTLV.TLV((byte) 0x7D, secretsProvider.getIv())); + input.add(new HuaweiTLV.TLV((byte) 0x7E, ciphertext)); + + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + huaweiTLV.decrypt(secretsProvider); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestVarInt.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestVarInt.java new file mode 100644 index 000000000..3bc298644 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestVarInt.java @@ -0,0 +1,72 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +public class TestVarInt { + + private void testValue(int intValue, byte[] bytesValue, int size) { + Assert.assertEquals(size, VarInt.getVarIntSize(intValue)); + Assert.assertEquals(intValue, VarInt.getVarIntValue(bytesValue, 0)); + Assert.assertArrayEquals(bytesValue, VarInt.putVarIntValue(intValue)); + + VarInt varInt = new VarInt(bytesValue, 0); + Assert.assertEquals(size, varInt.size); + Assert.assertEquals(intValue, varInt.dValue); + Assert.assertArrayEquals(bytesValue, varInt.eValue); + } + + @Test + public void testZero() { + testValue(0, new byte[]{0}, 1); + } + + @Test + public void testSingleValue() { + testValue(17, new byte[]{17}, 1); + } + + @Test + public void test0x80() { + // This is 1 << 8, the first 'overflowing' value + testValue(0x80, new byte[]{(byte) 0x81, 0x00}, 2); + } + + @Test + public void testDoubleValue() { + testValue(460, new byte[]{(byte) 0x83, 0x4C}, 2); + } + + @Test + public void testOffset() { + int intValue = 460; + byte[] bytesValue = {(byte) 0x83, 0x4C}; + byte[] bytesTest = {0x00, (byte) 0x83, 0x4C}; + int size = 2; + + Assert.assertEquals(size, VarInt.getVarIntSize(intValue)); + Assert.assertEquals(intValue, VarInt.getVarIntValue(bytesTest, 1)); + Assert.assertArrayEquals(bytesValue, VarInt.putVarIntValue(intValue)); + + VarInt varInt = new VarInt(bytesTest, 1); + Assert.assertEquals(size, varInt.size); + Assert.assertEquals(intValue, varInt.dValue); + Assert.assertArrayEquals(bytesValue, varInt.eValue); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestAlarms.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestAlarms.java new file mode 100644 index 000000000..c3cf58107 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestAlarms.java @@ -0,0 +1,167 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +public class TestAlarms { + + HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testEventAlarmsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + Field alarmField = Alarms.EventAlarmsRequest.class.getDeclaredField("alarms"); + alarmField.setAccessible(true); + + HuaweiTLV expectedAlarmsTlv = new HuaweiTLV(); + + Alarms.EventAlarmsRequest request = new Alarms.EventAlarmsRequest(paramsProvider); + + expectedAlarmsTlv.put(0x82, new HuaweiTLV() + .put(0x03, (byte) 1) + .put(0x04, true) + .put(0x05, (short) 0x1337) + .put(0x06, (byte) 0) + .put(0x07, "Alarm1") + ); + + request.addEventAlarm( + new Alarms.EventAlarm( + (byte) 1, + true, + (byte) 0x13, + (byte) 0x37, + (byte) 0, + "Alarm1" + ) + ); + + Assert.assertEquals(0x08, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + // TODO: check count in request + Assert.assertEquals(expectedAlarmsTlv, alarmField.get(request)); + + // A serialize will change the tlv, so we cannot test it here + + expectedAlarmsTlv.put(0x82,new HuaweiTLV() + .put(0x03, (byte) 2) + .put(0x04, false) + .put(0x05, (short) 0xCAFE) + .put(0x06, (byte) 1) + .put(0x07, "Alarm2") + ); + + request.addEventAlarm( + new Alarms.EventAlarm( + (byte) 2, + false, + (byte) 0xCA, + (byte) 0xFE, + (byte) 1, + "Alarm2" + ) + ); + + Assert.assertEquals(expectedAlarmsTlv, alarmField.get(request)); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, expectedAlarmsTlv); + + // Different order for better assertion messages in case of failure + List listOut = request.serialize(); + Assert.assertEquals(1, listOut.size()); + Assert.assertEquals(expectedAlarmsTlv, alarmField.get(request)); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + } + + @Test + public void testSmartAlarmRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + byte[] expectedOutput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x08, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0xcd, (byte) 0x7f, (byte) 0x80, (byte) 0x67, (byte) 0x02, (byte) 0x8d, (byte) 0x46, (byte) 0xfb, (byte) 0xc1, (byte) 0x0b, (byte) 0xed, (byte) 0x6c, (byte) 0x46, (byte) 0xb7, (byte) 0x59, (byte) 0xba, (byte) 0x08, (byte) 0xfd, (byte) 0xde, (byte) 0x3b, (byte) 0xee, (byte) 0x54, (byte) 0xbd, (byte) 0x4f, (byte) 0x27, (byte) 0xf6, (byte) 0x52, (byte) 0x9a, (byte) 0xae, (byte) 0xbf, (byte) 0x55, (byte) 0xd9, (byte) 0xe0, (byte) 0xa6}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x82, new HuaweiTLV() + .put(0x03, (byte) 0x01) + .put(0x04, true) + .put(0x05, (short) 0x1337) + .put(0x06, (byte) 1) + .put(0x07, (byte) 2) + ) + ); + + Alarms.SmartAlarmRequest request = new Alarms.SmartAlarmRequest( + paramsProvider, + new Alarms.SmartAlarm( + true, + (byte) 0x13, + (byte) 0x37, + (byte) 1, + (byte) 2 + ) + ); + + Assert.assertEquals(0x08, request.serviceId); + Assert.assertEquals(0x02, request.commandId); + Assert.assertTrue(request.complete); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expectedOutput, out.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestCalls.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestCalls.java new file mode 100644 index 000000000..c3cb3fd0b --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestCalls.java @@ -0,0 +1,68 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestCalls { + + @Test + public void testAnswerCallResponseAccept() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x04, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x02, (byte) 0x99, (byte) 0x6B}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, (byte) 0x02); + + HuaweiPacket packet = new HuaweiPacket(null).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x04, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Calls.AnswerCallResponse); + Assert.assertEquals(Calls.AnswerCallResponse.Action.CALL_ACCEPT, ((Calls.AnswerCallResponse) packet).action); + } + + @Test + public void testAnswerCallResponseReject() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x04, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0xA9, (byte) 0x08}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, (byte) 0x01); + + HuaweiPacket packet = new HuaweiPacket(null).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x04, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Calls.AnswerCallResponse); + Assert.assertEquals(Calls.AnswerCallResponse.Action.CALL_REJECT, ((Calls.AnswerCallResponse) packet).action); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDeviceConfig.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDeviceConfig.java new file mode 100644 index 000000000..c5badfd13 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDeviceConfig.java @@ -0,0 +1,498 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +public class TestDeviceConfig { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testLinkParamsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01) + .put(0x02) + .put(0x03) + .put(0x04); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x0b, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0xf1, (byte) 0x3b}; + DeviceConfig.LinkParams.Request request = new DeviceConfig.LinkParams.Request( + secretsProvider, HuaweiCoordinatorSupplier.HuaweiDeviceType.BLE + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testLinkParamsResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawLinkParams = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x1b, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x02, (byte) 0x00, (byte) 0x14, (byte) 0x05, (byte) 0x12, (byte) 0x00, (byte) 0x01, (byte) 0x6a, (byte) 0xa2, (byte) 0x96, (byte) 0xe3, (byte) 0x76, (byte) 0x41, (byte) 0xb1, (byte) 0x0c, (byte) 0xf8, (byte) 0xaa, (byte) 0xf7, (byte) 0x47, (byte) 0x05, (byte) 0x5d, (byte) 0x0a, (byte) 0xa3, (byte) 0xe8, (byte) 0x9f}; + + byte[] expectedServerNonceWithAuth = new byte[] {(byte) 0x00, (byte) 0x01, (byte) 0x6A, (byte) 0xA2, (byte) 0x96, (byte) 0xE3, (byte) 0x76, (byte) 0x41, (byte) 0xB1, (byte) 0x0C, (byte) 0xF8, (byte) 0xAA, (byte) 0xF7, (byte) 0x47, (byte) 0x05, (byte) 0x5D, (byte) 0x0A, (byte) 0xA3}; + byte[] expectedServerNonce = new byte[expectedServerNonceWithAuth.length - 2]; + System.arraycopy(expectedServerNonceWithAuth, 2, expectedServerNonce, 0, expectedServerNonceWithAuth.length - 2); + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x03, new byte[] {0x00, 0x14}) + .put(0x05, expectedServerNonceWithAuth); + + HuaweiPacket packetLinkParams = new HuaweiPacket(secretsProvider).parse(rawLinkParams); + packetLinkParams.parseTlv(); + + Assert.assertEquals(0x01, packetLinkParams.serviceId); + Assert.assertEquals(0x01, packetLinkParams.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetLinkParams)); + Assert.assertTrue(packetLinkParams.complete); + Assert.assertTrue(packetLinkParams instanceof DeviceConfig.LinkParams.Response); + Assert.assertEquals(0x01, ((DeviceConfig.LinkParams.Response) packetLinkParams).authVersion); + Assert.assertArrayEquals(expectedServerNonce, ((DeviceConfig.LinkParams.Response) packetLinkParams).serverNonce); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testLinkParamsResponseException() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawLinkParams = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x17, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x12, (byte) 0x00, (byte) 0x01, (byte) 0x6a, (byte) 0xa2, (byte) 0x96, (byte) 0xe3, (byte) 0x76, (byte) 0x41, (byte) 0xb1, (byte) 0x0c, (byte) 0xf8, (byte) 0xaa, (byte) 0xf7, (byte) 0x47, (byte) 0x05, (byte) 0x5d, (byte) 0x0a, (byte) 0xa3, (byte) 0xdd, (byte) 0x41}; + + HuaweiPacket packetLinkParams = new HuaweiPacket(secretsProvider).parse(rawLinkParams); + packetLinkParams.parseTlv(); + } + + @Test + public void testSupportedServicesRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte[] allSupportedServices = new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0a, (byte) 0x0b, (byte) 0x0c, (byte) 0x0d, (byte) 0x0e, (byte) 0x0f, (byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2a, (byte) 0x2b, (byte) 0x2c, (byte) 0x2d, (byte) 0x2e, (byte) 0x2f, (byte) 0x30, (byte) 0x31, (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x35}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, allSupportedServices); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0x14, (byte) 0xb1, (byte) 0x75, (byte) 0x7d, (byte) 0xc0, (byte) 0xa5, (byte) 0x32, (byte) 0xeb, (byte) 0xc1, (byte) 0x20, (byte) 0x7b, (byte) 0xb8, (byte) 0x59, (byte) 0xdb, (byte) 0xdb, (byte) 0xfe, (byte) 0x5b, (byte) 0x01, (byte) 0x0a, (byte) 0x7d, (byte) 0xb7, (byte) 0x76, (byte) 0xfc, (byte) 0xcc, (byte) 0x5f, (byte) 0x22, (byte) 0xff, (byte) 0x13, (byte) 0xcb, (byte) 0xbb, (byte) 0x4f, (byte) 0xe2, (byte) 0xcd, (byte) 0x6e, (byte) 0x4b, (byte) 0xd7, (byte) 0x7c, (byte) 0x05, (byte) 0x24, (byte) 0x85, (byte) 0x65, (byte) 0x5f, (byte) 0x95, (byte) 0x32, (byte) 0xb4, (byte) 0x5e, (byte) 0x16, (byte) 0xef, (byte) 0xad, (byte) 0x62, (byte) 0x38, (byte) 0xd5, (byte) 0x88, (byte) 0x63, (byte) 0xa4, (byte) 0xb0, (byte) 0x29, (byte) 0xbb, (byte) 0x90, (byte) 0x66, (byte) 0x8c, (byte) 0x3f, (byte) 0x58, (byte) 0x69, (byte) 0x40, (byte) 0x22}; + DeviceConfig.SupportedServices.Request request = new DeviceConfig.SupportedServices.Request( + secretsProvider, + allSupportedServices + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x02, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testSupportedServicesResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedServices = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0xC8, (byte) 0x9F, (byte) 0x1E, (byte) 0x2F, (byte) 0xE8, (byte) 0x31, (byte) 0xC8, (byte) 0x1E, (byte) 0x92, (byte) 0xB0, (byte) 0xE8, (byte) 0x9E, (byte) 0xC7, (byte) 0x2E, (byte) 0x76, (byte) 0xD7, (byte) 0x6C, (byte) 0x64, (byte) 0x22, (byte) 0x5A, (byte) 0x6C, (byte) 0xF9, (byte) 0xAF, (byte) 0xFB, (byte) 0x8E, (byte) 0x98, (byte) 0x74, (byte) 0xB6, (byte) 0xF9, (byte) 0x84, (byte) 0x3C, (byte) 0x1E, (byte) 0x3D, (byte) 0xCB, (byte) 0x7C, (byte) 0x23, (byte) 0x4F, (byte) 0x7B, (byte) 0x34, (byte) 0x0C, (byte) 0x49, (byte) 0xBD, (byte) 0x80, (byte) 0x94, (byte) 0x67, (byte) 0x1B, (byte) 0x5C, (byte) 0x64, (byte) 0x6B, (byte) 0xA4, (byte) 0xB9, (byte) 0xEC, (byte) 0xA7, (byte) 0x97, (byte) 0x95, (byte) 0x6F, (byte) 0x44, (byte) 0x13, (byte) 0x66, (byte) 0x7C, (byte) 0xF5, (byte) 0x9F, (byte) 0x05, (byte) 0x72, (byte) 0xc9, (byte) 0xe9}; + byte[] expectedSupportedServices = new byte[] {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x02, expectedSupportedServices); + + HuaweiPacket packetSupportedServices = new HuaweiPacket(secretsProvider).parse(rawSupportedServices); + packetSupportedServices.parseTlv(); + + Assert.assertEquals(0x01, packetSupportedServices.serviceId); + Assert.assertEquals(0x02, packetSupportedServices.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetSupportedServices)); + Assert.assertTrue(packetSupportedServices.complete); + Assert.assertTrue(packetSupportedServices instanceof DeviceConfig.SupportedServices.Response); + Assert.assertArrayEquals(expectedSupportedServices, ((DeviceConfig.SupportedServices.Response) packetSupportedServices).supportedServices); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testSupportedServicesResponseException() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedServices = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x5A, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x40, (byte) 0x28, (byte) 0xB0, (byte) 0x4D, (byte) 0x00, (byte) 0xCF, (byte) 0xE1, (byte) 0xD1, (byte) 0x7A, (byte) 0x5D, (byte) 0xE7, (byte) 0x61, (byte) 0x0E, (byte) 0xE1, (byte) 0xA5, (byte) 0xE8, (byte) 0xF9, (byte) 0x6D, (byte) 0x2D, (byte) 0x32, (byte) 0xB3, (byte) 0xC3, (byte) 0x7C, (byte) 0x07, (byte) 0xBC, (byte) 0x11, (byte) 0x03, (byte) 0x8A, (byte) 0x66, (byte) 0x8C, (byte) 0x47, (byte) 0x94, (byte) 0x86, (byte) 0x8C, (byte) 0x0D, (byte) 0xC6, (byte) 0xBC, (byte) 0xDF, (byte) 0xB3, (byte) 0x00, (byte) 0xFB, (byte) 0x68, (byte) 0x11, (byte) 0xC1, (byte) 0xB3, (byte) 0x66, (byte) 0x6D, (byte) 0x85, (byte) 0x6F, (byte) 0xF0, (byte) 0xA9, (byte) 0xD0, (byte) 0x49, (byte) 0xDF, (byte) 0xF5, (byte) 0x82, (byte) 0x01, (byte) 0x9F, (byte) 0xE4, (byte) 0x60, (byte) 0x36, (byte) 0x81, (byte) 0xAA, (byte) 0x31, (byte) 0xA1, (byte) 0x39, (byte) 0xD6}; + + HuaweiPacket packetSupportedServices = new HuaweiPacket(secretsProvider).parse(rawSupportedServices); + packetSupportedServices.parseTlv(); + } + + @Test + public void testSupportedCommandsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte service1 = (byte) 0x01; + byte[] commands1 = new byte[]{(byte) 0x04, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0D, (byte) 0x0E, (byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, (byte) 0x14, (byte) 0x1B, (byte) 0x1A, (byte) 0x1D, (byte) 0x21, (byte) 0x22, (byte) 0x23, (byte) 0x24, (byte) 0x29, (byte) 0x2A, (byte) 0x2B, (byte) 0x32, (byte) 0x2E, (byte) 0x31, (byte) 0x30, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x2F}; + byte service2 = (byte) 0x02; + byte[] commands2 = new byte[]{(byte) 0x01, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08}; + byte service3 = (byte) 0x04; + byte[] commands3 = new byte[]{(byte) 0x01}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, service1) + .put(0x03, commands1) + .put(0x02, service2) + .put(0x03, commands2) + .put(0x02, service3) + .put(0x03, commands3) + ); + + byte[] expectedSerialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x5A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x40, (byte) 0x81, (byte) 0xED, (byte) 0x07, (byte) 0xF6, (byte) 0x51, (byte) 0xA3, (byte) 0x19, (byte) 0xBD, (byte) 0xCE, (byte) 0x35, (byte) 0x13, (byte) 0x23, (byte) 0x1E, (byte) 0xFC, (byte) 0x1A, (byte) 0x51, (byte) 0x92, (byte) 0xBB, (byte) 0x43, (byte) 0xC5, (byte) 0xF5, (byte) 0xD9, (byte) 0x4E, (byte) 0xCC, (byte) 0x2F, (byte) 0xE0, (byte) 0xDB, (byte) 0xB1, (byte) 0x5E, (byte) 0x78, (byte) 0x66, (byte) 0x69, (byte) 0x61, (byte) 0x85, (byte) 0x46, (byte) 0xB2, (byte) 0x50, (byte) 0xEC, (byte) 0xB5, (byte) 0x3F, (byte) 0x74, (byte) 0x68, (byte) 0x47, (byte) 0x03, (byte) 0x87, (byte) 0xC1, (byte) 0xB3, (byte) 0x53, (byte) 0x7B, (byte) 0x53, (byte) 0xDB, (byte) 0xE8, (byte) 0x5E, (byte) 0x82, (byte) 0x56, (byte) 0xFD, (byte) 0x16, (byte) 0x66, (byte) 0x03, (byte) 0xB2, (byte) 0x56, (byte) 0xA3, (byte) 0x14, (byte) 0x70, (byte) 0x38, (byte) 0x3E}; + DeviceConfig.SupportedCommands.Request request = new DeviceConfig.SupportedCommands.Request( + secretsProvider + ); + request.addCommandsForService(service1, commands1); + request.addCommandsForService(service2, commands2); + request.addCommandsForService(service3, commands3); + + List out = request.serialize(); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x03, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expectedSerialized, out.get(0)); + } + + @Test + public void testSupportedCommandsResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedCommands = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x5A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x40, (byte) 0x8B, (byte) 0x86, (byte) 0x29, (byte) 0xE3, (byte) 0x90, (byte) 0x25, (byte) 0x4E, (byte) 0x14, (byte) 0xE1, (byte) 0xDD, (byte) 0x96, (byte) 0x63, (byte) 0x66, (byte) 0xB8, (byte) 0x1E, (byte) 0x4A, (byte) 0xC1, (byte) 0xA7, (byte) 0x49, (byte) 0xB0, (byte) 0x9F, (byte) 0x21, (byte) 0x7C, (byte) 0xE8, (byte) 0x2C, (byte) 0x72, (byte) 0x93, (byte) 0x9F, (byte) 0xAC, (byte) 0x37, (byte) 0x3B, (byte) 0x4D, (byte) 0x1A, (byte) 0xCB, (byte) 0xC2, (byte) 0xFF, (byte) 0x64, (byte) 0xE5, (byte) 0xF0, (byte) 0x3E, (byte) 0x5B, (byte) 0xFF, (byte) 0xB1, (byte) 0x9C, (byte) 0x59, (byte) 0xB2, (byte) 0xF1, (byte) 0xD6, (byte) 0x4B, (byte) 0x2B, (byte) 0x99, (byte) 0xFB, (byte) 0xEA, (byte) 0x29, (byte) 0x66, (byte) 0xD3, (byte) 0x90, (byte) 0x0B, (byte) 0xC9, (byte) 0xF0, (byte) 0xB4, (byte) 0x9B, (byte) 0x3B, (byte) 0x3E, (byte) 0x50, (byte) 0xFA}; + + List expectedSupportedCommandsList = new ArrayList<>(); + DeviceConfig.SupportedCommands.Response.CommandsList commandsList = new DeviceConfig.SupportedCommands.Response.CommandsList(); + commandsList.service = 1; + commandsList.commands = new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x09, (byte) 0x0A, (byte) 0x0B, (byte) 0x0D, (byte) 0x0F, (byte) 0x10, (byte) 0x11}; + expectedSupportedCommandsList.add(commandsList); + commandsList.service = 2; + commandsList.commands = new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x06}; + expectedSupportedCommandsList.add(commandsList); + commandsList.service = 4; + commandsList.commands = new byte[] {(byte) 0x01}; + expectedSupportedCommandsList.add(commandsList); + + byte[] expectedSupportedCommands = new byte[] {(byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x04, (byte) 0x1E, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x06, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x01, (byte) 0x04, (byte) 0x04, (byte) 0x01, (byte) 0x01}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, expectedSupportedCommands); + + HuaweiPacket packetSupportedCommands = new HuaweiPacket(secretsProvider).parse(rawSupportedCommands); + packetSupportedCommands.parseTlv(); + + Assert.assertEquals(0x01, packetSupportedCommands.serviceId); + Assert.assertEquals(0x03, packetSupportedCommands.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetSupportedCommands)); + Assert.assertTrue(packetSupportedCommands.complete); + Assert.assertTrue(packetSupportedCommands instanceof DeviceConfig.SupportedCommands.Response); + Assert.assertEquals(expectedSupportedCommandsList.size(), ((DeviceConfig.SupportedCommands.Response) packetSupportedCommands).commandsLists.size()); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testSupportedCommandsResponseException02() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedCommands = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x4A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x30, (byte) 0x57, (byte) 0xC7, (byte) 0xF7, (byte) 0x47, (byte) 0x0E, (byte) 0xA9, (byte) 0xA2, (byte) 0x9E, (byte) 0xF3, (byte) 0xB0, (byte) 0xBD, (byte) 0x02, (byte) 0xE0, (byte) 0x79, (byte) 0x3C, (byte) 0x12, (byte) 0xE6, (byte) 0x58, (byte) 0xDA, (byte) 0xF7, (byte) 0x0B, (byte) 0xC3, (byte) 0x93, (byte) 0x8D, (byte) 0x37, (byte) 0x2E, (byte) 0xA9, (byte) 0xB8, (byte) 0xF8, (byte) 0xF7, (byte) 0x97, (byte) 0xF3, (byte) 0x22, (byte) 0x08, (byte) 0xDF, (byte) 0xAD, (byte) 0x2B, (byte) 0x62, (byte) 0x33, (byte) 0x11, (byte) 0x93, (byte) 0x66, (byte) 0xD1, (byte) 0xAE, (byte) 0xF3, (byte) 0x02, (byte) 0x18, (byte) 0x49, (byte) 0xD0, (byte) 0x40}; + + HuaweiPacket packetSupportedCommands = new HuaweiPacket(secretsProvider).parse(rawSupportedCommands); + packetSupportedCommands.parseTlv(); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testSupportedCommandsResponseException04() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedCommands = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x2A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x10, (byte) 0x25, (byte) 0x26, (byte) 0xE0, (byte) 0x79, (byte) 0x1D, (byte) 0x19, (byte) 0x82, (byte) 0xDB, (byte) 0x0A, (byte) 0x3A, (byte) 0x21, (byte) 0x6E, (byte) 0x70, (byte) 0x52, (byte) 0xAB, (byte) 0xF3, (byte) 0x14, (byte) 0xB4}; + + HuaweiPacket packetSupportedCommands = new HuaweiPacket(secretsProvider).parse(rawSupportedCommands); + packetSupportedCommands.parseTlv(); + } + + @Test + public void testDateFormatRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte dateformat = (byte) 0x02; + byte timeFormat = (byte) 0x02; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, dateformat) + .put(0x03, timeFormat) + ); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x63, (byte) 0xe0, (byte) 0xf1, (byte) 0xbf, (byte) 0x40, (byte) 0xab, (byte) 0x09, (byte) 0x63, (byte) 0x51, (byte) 0x7c, (byte) 0xa7, (byte) 0x8c, (byte) 0x2e, (byte) 0xd9, (byte) 0x6a, (byte) 0x6c, (byte) 0xdc, (byte) 0xe9}; + DeviceConfig.DateFormat.Request request = new DeviceConfig.DateFormat.Request ( + secretsProvider, + dateformat, + timeFormat + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x04, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testTimeRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int timestamp = 1633987331; + short zoneOffset = (short) 512; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, timestamp) + .put(0x02, zoneOffset); + + byte[] serialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x2A, (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x10, (byte) 0xED, (byte) 0x67, (byte) 0x61, (byte) 0x8A, (byte) 0x8E, (byte) 0x44, (byte) 0x67, (byte) 0xB1, (byte) 0x2A, (byte) 0xB4, (byte) 0xFA, (byte) 0x86, (byte) 0x76, (byte) 0x17, (byte) 0x8C, (byte) 0x61, (byte) 0xFC, (byte) 0x99}; + DeviceConfig.TimeRequest request = new DeviceConfig.TimeRequest(secretsProvider); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x05, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testProductInformationRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV(); + byte[] expectedTags = {0x01, 0x02, 0x07, 0x09, 0x0A, 0x11, 0x12, 0x16, 0x1A, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23}; + for (byte tag : expectedTags) { + expectedTlv.put(tag); + } + + // Outdated + //byte[] serialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x3A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x20, (byte) 0x10, (byte) 0x9B, (byte) 0x27, (byte) 0x5D, (byte) 0xB1, (byte) 0x3C, (byte) 0xFD, (byte) 0x40, (byte) 0x4B, (byte) 0xA8, (byte) 0xAC, (byte) 0xAF, (byte) 0x8A, (byte) 0xB6, (byte) 0xA5, (byte) 0x3D, (byte) 0x40, (byte) 0x30, (byte) 0x2C, (byte) 0x79, (byte) 0x98, (byte) 0x6D, (byte) 0xEC, (byte) 0xD1, (byte) 0x39, (byte) 0xE6, (byte) 0xFE, (byte) 0x5C, (byte) 0xE8, (byte) 0xB2, (byte) 0xF3, (byte) 0x9E, (byte) 0x3E, (byte) 0x1B}; + DeviceConfig.ProductInfo.Request request = new DeviceConfig.ProductInfo.Request ( + secretsProvider, HuaweiCoordinatorSupplier.HuaweiDeviceType.BLE + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x07, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + // Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testProductInformationResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawProductInformation = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x4A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x30, (byte) 0x50, (byte) 0x75, (byte) 0xA5, (byte) 0x4F, (byte) 0x26, (byte) 0xF7, (byte) 0x74, (byte) 0x0B, (byte) 0xB2, (byte) 0xD8, (byte) 0x01, (byte) 0xBA, (byte) 0xDC, (byte) 0x7E, (byte) 0x40, (byte) 0x36, (byte) 0xD5, (byte) 0x6D, (byte) 0x4B, (byte) 0x7B, (byte) 0x8F, (byte) 0xC6, (byte) 0xFB, (byte) 0x48, (byte) 0xFC, (byte) 0x89, (byte) 0x54, (byte) 0xF8, (byte) 0xBB, (byte) 0xC0, (byte) 0x48, (byte) 0x9E, (byte) 0x34, (byte) 0x0E, (byte) 0xB1, (byte) 0x24, (byte) 0xD8, (byte) 0x89, (byte) 0x02, (byte) 0x7E, (byte) 0x6C, (byte) 0x3E, (byte) 0x81, (byte) 0x7D, (byte) 0x38, (byte) 0x0F, (byte) 0xD9, (byte) 0x2A, (byte) 0x98, (byte) 0xE3}; + String softwareVersion = "1.0.10.78"; + String productModel = "Crius"; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x03, new byte[] {(byte) 0x4E, (byte) 0x41}) + .put(0x07, new byte[] {(byte) 0x31, (byte) 0x2E, (byte) 0x30, (byte) 0x2E, (byte) 0x31, (byte) 0x30, (byte) 0x2E, (byte) 0x37, (byte) 0x38}) + .put(0x0A, new byte[] {(byte) 0x43, (byte) 0x72, (byte) 0x69, (byte) 0x75, (byte) 0x73, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}); + + HuaweiPacket packetProductInformation = new HuaweiPacket(secretsProvider).parse(rawProductInformation); + packetProductInformation.parseTlv(); + + Assert.assertEquals(0x01, packetProductInformation.serviceId); + Assert.assertEquals(0x07, packetProductInformation.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetProductInformation)); + Assert.assertTrue(packetProductInformation.complete); + Assert.assertTrue(packetProductInformation instanceof DeviceConfig.ProductInfo.Response); + Assert.assertTrue(softwareVersion.equals(((DeviceConfig.ProductInfo.Response) packetProductInformation).softwareVersion)); + System.out.println(((DeviceConfig.ProductInfo.Response) packetProductInformation).productModel); + System.out.println(((DeviceConfig.ProductInfo.Response) packetProductInformation).productModel.length()); + Assert.assertTrue(productModel.equals(((DeviceConfig.ProductInfo.Response) packetProductInformation).productModel)); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testProductInformationResponseException07() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawProductInformation = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x3A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x20, (byte) 0xFE, (byte) 0x24, (byte) 0x4C, (byte) 0x68, (byte) 0xFC, (byte) 0x1D, (byte) 0xAD, (byte) 0x64, (byte) 0x77, (byte) 0xC9, (byte) 0xE9, (byte) 0x26, (byte) 0x8D, (byte) 0x3C, (byte) 0x3C, (byte) 0x8C, (byte) 0xB6, (byte) 0xA6, (byte) 0xF1, (byte) 0xBF, (byte) 0xAC, (byte) 0xB6, (byte) 0x7A, (byte) 0x75, (byte) 0xA3, (byte) 0xA9, (byte) 0x07, (byte) 0x5F, (byte) 0x39, (byte) 0x0F, (byte) 0x28, (byte) 0x61, (byte) 0x50, (byte) 0x61}; + + HuaweiPacket packetProductInformation = new HuaweiPacket(secretsProvider).parse(rawProductInformation); + packetProductInformation.parseTlv(); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testProductInformationResponseException0A() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawProductInformation = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x2A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x10, (byte) 0xB6, (byte) 0x67, (byte) 0xA5, (byte) 0x6A, (byte) 0x46, (byte) 0x0F, (byte) 0x08, (byte) 0x1E, (byte) 0xAC, (byte) 0x1E, (byte) 0x6B, (byte) 0xF2, (byte) 0x11, (byte) 0x4A, (byte) 0x54, (byte) 0x20, (byte) 0xCF, (byte) 0xB6}; + + HuaweiPacket packetProductInformation = new HuaweiPacket(secretsProvider).parse(rawProductInformation); + packetProductInformation.parseTlv(); + } + + @Test + public void testBondRequest() throws NoSuchFieldException, IllegalAccessException { + byte[] clientSerial = new byte[] {(byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44}; + String mac = "FF:FF:FF:FF:FF:CC"; + HuaweiCrypto huaweiCrypto = new HuaweiCrypto(0x01); + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + try { + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01) + .put(0x03, (byte) 0x00) + .put(0x05, clientSerial) + .put(0x06, huaweiCrypto.encryptBondingKey(secretsProvider.getSecretKey(), mac, secretsProvider.getIv())) + .put(0x07, secretsProvider.getIv()); + + byte[] serialized = new byte[]{(byte) 0x5A, (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x01, (byte) 0x0E, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x05, (byte) 0x06, (byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44, (byte) 0x06, (byte) 0x20, (byte) 0x88, (byte) 0x45, (byte) 0xAA, (byte) 0xB5, (byte) 0x9C, (byte) 0x84, (byte) 0x39, (byte) 0xAE, (byte) 0xD8, (byte) 0xE9, (byte) 0x71, (byte) 0x01, (byte) 0x5D, (byte) 0xC8, (byte) 0x34, (byte) 0x05, (byte) 0xC5, (byte) 0x9A, (byte) 0x6B, (byte) 0xDB, (byte) 0x62, (byte) 0x7D, (byte) 0xC8, (byte) 0xC3, (byte) 0xF4, (byte) 0xCC, (byte) 0x30, (byte) 0x74, (byte) 0x21, (byte) 0xD4, (byte) 0x45, (byte) 0x0E, (byte) 0x07, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x72, (byte) 0xFC}; + DeviceConfig.Bond.Request request = new DeviceConfig.Bond.Request( + secretsProvider, + clientSerial, + mac, + huaweiCrypto + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x0E, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | HuaweiPacket.CryptoException e) { + e.printStackTrace(); + Assert.fail(); + } + } + + @Test + public void testBondParamsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte[] clientSerial = new byte[] {(byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44}; + byte[] mac = new byte[] {(byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x43, (byte) 0x43}; + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01) + .put(0x03, clientSerial) + .put(0x04, (byte) 0x02) + .put(0x05) + .put(0x07, mac) + .put(0x09); + + byte[] serialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x27, (byte) 0x00, (byte) 0x01, (byte) 0x0F, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x06, (byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44, (byte) 0x04, (byte) 0x01, (byte) 0x02, (byte) 0x05, (byte) 0x00, (byte) 0x07, (byte) 0x11, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x43, (byte) 0x43, (byte) 0x09, (byte) 0x00, (byte) 0xE5, (byte) 0xD8}; + DeviceConfig.BondParams.Request request = new DeviceConfig.BondParams.Request( + secretsProvider, + clientSerial, + mac + ); + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x0F, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testAuthRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte[] challenge = new byte[] {(byte) 0x9D, (byte) 0xF6, (byte) 0x52, (byte) 0x69, (byte) 0x06, (byte) 0x7B, (byte) 0xEB, (byte) 0x46, (byte) 0x94, (byte) 0xAD, (byte) 0x35, (byte) 0xE2, (byte) 0x88, (byte) 0xC3, (byte) 0x84, (byte) 0x24, (byte) 0xA2, (byte) 0x55, (byte) 0xD8, (byte) 0x0F, (byte) 0xA7, (byte) 0x68, (byte) 0x21, (byte) 0x9B, (byte) 0xA1, (byte) 0xC3, (byte) 0xDC, (byte) 0x09, (byte) 0x24, (byte) 0x81, (byte) 0x51, (byte) 0x61}; + byte[] nonce = new byte[] {(byte) 0x00, (byte) 0x01, (byte) 0xBF, (byte) 0x1F, (byte) 0xEF, (byte) 0x9F, (byte) 0xF0, (byte) 0xFE, (byte) 0xEF, (byte) 0xEF, (byte) 0x9F, (byte) 0xEF, (byte) 0xF0, (byte) 0xEF, (byte) 0xF8, (byte) 0xFA, (byte) 0xEF, (byte) 0xF0}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, challenge) + .put(0x02, nonce); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x39, (byte) 0x00, (byte) 0x01, (byte) 0x13, (byte) 0x01, (byte) 0x20, (byte) 0x9d, (byte) 0xf6, (byte) 0x52, (byte) 0x69, (byte) 0x06, (byte) 0x7b, (byte) 0xeb, (byte) 0x46, (byte) 0x94, (byte) 0xad, (byte) 0x35, (byte) 0xe2, (byte) 0x88, (byte) 0xc3, (byte) 0x84, (byte) 0x24, (byte) 0xa2, (byte) 0x55, (byte) 0xd8, (byte) 0x0f, (byte) 0xa7, (byte) 0x68, (byte) 0x21, (byte) 0x9b, (byte) 0xa1, (byte) 0xc3, (byte) 0xdc, (byte) 0x09, (byte) 0x24, (byte) 0x81, (byte) 0x51, (byte) 0x61, (byte) 0x02, (byte) 0x12, (byte) 0x00, (byte) 0x01, (byte) 0xbf, (byte) 0x1f, (byte) 0xef, (byte) 0x9f, (byte) 0xf0, (byte) 0xfe, (byte) 0xef, (byte) 0xef, (byte) 0x9f, (byte) 0xef, (byte) 0xf0, (byte) 0xef, (byte) 0xf8, (byte) 0xfa, (byte) 0xef, (byte) 0xf0, (byte) 0xdc, (byte) 0x88}; + DeviceConfig.Auth.Request request = new DeviceConfig.Auth.Request( + secretsProvider, + challenge, + nonce, + false + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x13, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDisconnectNotification.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDisconnectNotification.java new file mode 100644 index 000000000..a9b30969a --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDisconnectNotification.java @@ -0,0 +1,104 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestDisconnectNotification { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testBluetoothDisconnectNotificationSetting() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvEnable = new HuaweiTLV() + .put(0x01, true); + + byte[] serializedEnable = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0b, (byte) 0x03, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xcd, (byte) 0x97, (byte) 0x7e, (byte) 0x01, (byte) 0x48, (byte) 0x34, (byte) 0x2a, (byte) 0x48, (byte) 0x58, (byte) 0x0d, (byte) 0x30, (byte) 0xc7, (byte) 0xbc, (byte) 0x2e, (byte) 0x40, (byte) 0xd4, (byte) 0x20, (byte) 0xaf}; + + DisconnectNotification.DisconnectNotificationSetting.Request requestEnable = new DisconnectNotification.DisconnectNotificationSetting.Request( + secretsProvider, + true + ); + + Assert.assertEquals(0x0b, requestEnable.serviceId); + Assert.assertEquals(0x03, requestEnable.commandId); + Assert.assertEquals(expectedTlvEnable, tlvField.get(requestEnable)); + Assert.assertTrue(requestEnable.complete); + List outEnable = requestEnable.serialize(); + Assert.assertEquals(1, outEnable.size()); + Assert.assertArrayEquals(serializedEnable, outEnable.get(0)); + + + HuaweiTLV expectedTlvDisable = new HuaweiTLV() + .put(0x01, false); + + byte[] serializedDisable = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0b, (byte) 0x03, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x39, (byte) 0x9D}; + + DisconnectNotification.DisconnectNotificationSetting.Request requestDisable = new DisconnectNotification.DisconnectNotificationSetting.Request( + secretsProvider, + false + ); + + Assert.assertEquals(0x0b, requestDisable.serviceId); + Assert.assertEquals(0x03, requestDisable.commandId); + Assert.assertEquals(expectedTlvDisable, tlvField.get(requestDisable)); + Assert.assertTrue(requestDisable.complete); + List outDisable = requestDisable.serialize(); + Assert.assertEquals(1, outDisable.size()); + Assert.assertArrayEquals(serializedDisable, outDisable.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFindPhone.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFindPhone.java new file mode 100644 index 000000000..3aa91ddbe --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFindPhone.java @@ -0,0 +1,120 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestFindPhone { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testStartFindPhone() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0xcc, (byte) 0xf1}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, true); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FindPhone.Response); + Assert.assertTrue(((FindPhone.Response) packet).start); + } + + @Test + public void testStopFindPhone() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xdc, (byte) 0xd0}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, false); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FindPhone.Response); + Assert.assertFalse(((FindPhone.Response) packet).start); + } + + @Test + public void testFindPhoneMissingTag() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0xa1, (byte) 0x91}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV(); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FindPhone.Response); + Assert.assertFalse(((FindPhone.Response) packet).start); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFitnessData.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFitnessData.java new file mode 100644 index 000000000..6b4fb760e --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFitnessData.java @@ -0,0 +1,502 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestFitnessData { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testMessageCountRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int startSleep = 0x00000000; + int endSleep = 0x01020304; + int startStep = 0x01020304; + int endStep = 0x10203040; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvSleep = new HuaweiTLV() + .put(0x81) + .put(0x03, startSleep) + .put(0x04, endSleep); + HuaweiTLV expectedTlvStep = new HuaweiTLV() + .put(0x81) + .put(0x03, startStep) + .put(0x04, endStep); + + byte[] sleepSerialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x32, (byte) 0x69, (byte) 0x7b, (byte) 0x51, (byte) 0x85, (byte) 0x20, (byte) 0x9b, (byte) 0x16, (byte) 0x6b, (byte) 0x93, (byte) 0x8a, (byte) 0x3d, (byte) 0xd5, (byte) 0x9a, (byte) 0xf9, (byte) 0x29, (byte) 0xdf, (byte) 0x07}; + byte[] stepSerialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x4d, (byte) 0x52, (byte) 0x79, (byte) 0x57, (byte) 0x49, (byte) 0x30, (byte) 0x75, (byte) 0xc6, (byte) 0x28, (byte) 0x5b, (byte) 0x79, (byte) 0xd5, (byte) 0xab, (byte) 0x89, (byte) 0x0d, (byte) 0x1e, (byte) 0xa9, (byte) 0xc9}; + + FitnessData.MessageCount.Request sleepRequest = new FitnessData.MessageCount.Request(secretsProvider, FitnessData.MessageCount.sleepId, startSleep, endSleep); + FitnessData.MessageCount.Request stepRequest = new FitnessData.MessageCount.Request(secretsProvider, FitnessData.MessageCount.stepId, startStep, endStep); + + Assert.assertEquals(0x07, sleepRequest.serviceId); + Assert.assertEquals(0x0c, sleepRequest.commandId); + Assert.assertEquals(expectedTlvSleep, tlvField.get(sleepRequest)); + Assert.assertTrue(sleepRequest.complete); + List outSleep = sleepRequest.serialize(); + Assert.assertEquals(1, outSleep.size()); + Assert.assertArrayEquals(sleepSerialized, outSleep.get(0)); + + Assert.assertEquals(0x07, stepRequest.serviceId); + Assert.assertEquals(0x0a, stepRequest.commandId); + Assert.assertEquals(expectedTlvStep, tlvField.get(stepRequest)); + Assert.assertTrue(stepRequest.complete); + List outStep = stepRequest.serialize(); + Assert.assertEquals(1, outStep.size()); + Assert.assertArrayEquals(stepSerialized, outStep.get(0)); + } + + @Test + public void testMessageCountResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSleep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x93, (byte) 0x09}; + byte[] rawStep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0xd4, (byte) 0x93}; + + short expectedCount = 0x1337; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV().put(0x02, expectedCount)); + + HuaweiPacket packetSleep = new HuaweiPacket(secretsProvider).parse(rawSleep); + HuaweiPacket packetStep = new HuaweiPacket(secretsProvider).parse(rawStep); + packetSleep.parseTlv(); + packetStep.parseTlv(); + + Assert.assertEquals(0x07, packetSleep.serviceId); + Assert.assertEquals(0x0c, packetSleep.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetSleep)); + Assert.assertTrue(packetSleep.complete); + Assert.assertTrue(packetSleep instanceof FitnessData.MessageCount.Response); + Assert.assertEquals(expectedCount, ((FitnessData.MessageCount.Response) packetSleep).count); + + Assert.assertEquals(0x07, packetStep.serviceId); + Assert.assertEquals(0x0a, packetStep.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetStep)); + Assert.assertTrue(packetStep.complete); + Assert.assertTrue(packetStep instanceof FitnessData.MessageCount.Response); + Assert.assertEquals(expectedCount, ((FitnessData.MessageCount.Response) packetStep).count); + } + + @Test + public void testMessageDataRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiCrypto.CryptoException, HuaweiPacket.CryptoException { + short count = 0x1337; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV().put(0x02, count)); + expectedTlv.encrypt(secretsProvider); + + byte[] expectedSleep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0d, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x7d, (byte) 0xad}; + byte[] expectedStep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x3a, (byte) 0x37}; + + FitnessData.MessageData.Request sleepRequest = new FitnessData.MessageData.Request(secretsProvider, FitnessData.MessageData.sleepId, count); + FitnessData.MessageData.Request stepRequest = new FitnessData.MessageData.Request(secretsProvider, FitnessData.MessageData.stepId, count); + + Assert.assertEquals(0x07, sleepRequest.serviceId); + Assert.assertEquals(0x0d, sleepRequest.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(sleepRequest)); + Assert.assertTrue(sleepRequest.complete); + List outSleep = sleepRequest.serialize(); + Assert.assertEquals(1, outSleep.size()); + Assert.assertArrayEquals(expectedSleep, outSleep.get(0)); + + Assert.assertEquals(0x07, stepRequest.serviceId); + Assert.assertEquals(0x0b, stepRequest.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(stepRequest)); + Assert.assertTrue(stepRequest.complete); + List outStep = stepRequest.serialize(); + Assert.assertEquals(1, outStep.size()); + Assert.assertArrayEquals(expectedStep, outStep.get(0)); + } + + @Test + public void testMessageDataSleepResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x0d, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0xa4, (byte) 0x9e, (byte) 0xd8, (byte) 0xd3, (byte) 0x7a, (byte) 0x0e, (byte) 0x51, (byte) 0x55, (byte) 0xc5, (byte) 0x48, (byte) 0x07, (byte) 0x99, (byte) 0xf5, (byte) 0x99, (byte) 0x48, (byte) 0x3e, (byte) 0x41, (byte) 0xed, (byte) 0x16, (byte) 0xf1, (byte) 0x52, (byte) 0xd2, (byte) 0x9f, (byte) 0x38, (byte) 0xe8, (byte) 0xb1, (byte) 0x83, (byte) 0xd6, (byte) 0xcb, (byte) 0x52, (byte) 0xb0, (byte) 0x9f, (byte) 0x48, (byte) 0x05}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x83, new HuaweiTLV() + .put(0x04, (byte) 0x00) + .put(0x05, new byte[] {}) + ) + .put(0x83, new HuaweiTLV() + .put(0x04, (byte) 0x01) + .put(0x05, new byte[] {0x01, 0x02}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0d, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.SleepResponse); + Assert.assertEquals(0x1337, ((FitnessData.MessageData.SleepResponse) packet).number); + Assert.assertEquals(2, ((FitnessData.MessageData.SleepResponse) packet).containers.size()); + Assert.assertEquals(0x00, ((FitnessData.MessageData.SleepResponse) packet).containers.get(0).type); + Assert.assertArrayEquals(new byte[] {}, ((FitnessData.MessageData.SleepResponse) packet).containers.get(0).timestamp); + Assert.assertEquals(0x01, ((FitnessData.MessageData.SleepResponse) packet).containers.get(1).type); + Assert.assertArrayEquals(new byte[] {0x01, 0x02}, ((FitnessData.MessageData.SleepResponse) packet).containers.get(1).timestamp); + } + + @Test + public void testMessageDataStepResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0xdc, (byte) 0xb7, (byte) 0xf6, (byte) 0xaa, (byte) 0xb2, (byte) 0xf1, (byte) 0x03, (byte) 0x53, (byte) 0x25, (byte) 0x39, (byte) 0xe4, (byte) 0x79, (byte) 0xdd, (byte) 0xbf, (byte) 0x18, (byte) 0x7b, (byte) 0x98, (byte) 0x30, (byte) 0xb7, (byte) 0x4c, (byte) 0x33, (byte) 0xd2, (byte) 0x0c, (byte) 0xa5, (byte) 0xee, (byte) 0xfe, (byte) 0x5f, (byte) 0xa5, (byte) 0x12, (byte) 0x20, (byte) 0xec, (byte) 0x79, (byte) 0x38, (byte) 0xec, (byte) 0x9e, (byte) 0x4d, (byte) 0xfc, (byte) 0xc3, (byte) 0x5c, (byte) 0x59, (byte) 0x67, (byte) 0x51, (byte) 0x4b, (byte) 0xef, (byte) 0x50, (byte) 0x48, (byte) 0xb7, (byte) 0xf8, (byte) 0xc7, (byte) 0xe3, (byte) 0xf7, (byte) 0xdf, (byte) 0x82, (byte) 0xb4, (byte) 0x1a, (byte) 0xb8, (byte) 0x94, (byte) 0x78, (byte) 0x0d, (byte) 0xda, (byte) 0x53, (byte) 0xe3, (byte) 0xbe, (byte) 0xbf, (byte) 0x21, (byte) 0xc2}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + // TODO: add HR data + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x03, 0xCAFEBEEF) + .put(0x084, new HuaweiTLV() + .put(0x05, (byte) 0x00) + .put(0x06, new byte[] {}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x01) + .put(0x06, new byte[] {0x01, 0x02}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x02) + .put(0x06, new byte[] {0x0e, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x02) + .put(0x06, new byte[] {0x01, 0x00, 0x01}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0b, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.StepResponse); + + Assert.assertEquals(0x1337, ((FitnessData.MessageData.StepResponse) packet).number); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).timestamp); + Assert.assertEquals(4, ((FitnessData.MessageData.StepResponse) packet).containers.size()); + + Assert.assertEquals(0x00, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestampOffset); + Assert.assertArrayEquals(new byte[] {}, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).data); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestamp); + Assert.assertNull(((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData); + Assert.assertEquals("Data is missing feature bitmap.", ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).distance); + Assert.assertNull(((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs); + + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x01, 0x02}, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).data); + Assert.assertEquals(0xCAFEBF2B, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestamp); + Assert.assertNull(((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData); + Assert.assertEquals("Data is too short for selected features.", ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).distance); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.size()); + + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x0e, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03}, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).data); + Assert.assertEquals(0xCAFEBF67, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).timestamp); + Assert.assertEquals(3, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(0).bitmap); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(0).value); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(1).bitmap); + Assert.assertEquals(0x04, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(1).tag); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(1).value); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(2).bitmap); + Assert.assertEquals(0x08, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(2).tag); + Assert.assertEquals(0x03, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(2).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedDataError); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).steps); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).calories); + Assert.assertEquals(0x03, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).distance); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).unknownTVs.size()); + + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x01, 0x00, 0x01}, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).data); + Assert.assertEquals(0xCAFEBF67, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.get(0).bitmap); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).distance); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.get(0).bitmap); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.get(0).value); + } + + @Test + public void testMessageDataStepResponseSingleByte() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0xcf, (byte) 0xa7, (byte) 0x76, (byte) 0x30, (byte) 0x69, (byte) 0xa3, (byte) 0x83, (byte) 0x6e, (byte) 0xd2, (byte) 0x84, (byte) 0x70, (byte) 0xc8, (byte) 0xca, (byte) 0x94, (byte) 0x87, (byte) 0xd2, (byte) 0x0d, (byte) 0x1e, (byte) 0xf5, (byte) 0x60, (byte) 0x72, (byte) 0xa4, (byte) 0xd9, (byte) 0x8f, (byte) 0xf6, (byte) 0xdf, (byte) 0x09, (byte) 0x35, (byte) 0x3c, (byte) 0x86, (byte) 0x62, (byte) 0x00, (byte) 0x0a, (byte) 0x3b}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + // TODO: change test as 0x40 is now added as HR + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x01) + .put(0x03, 0x02) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x00) + .put(0x06, new byte[] {0x20, 0x01}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x01) + .put(0x06, new byte[] {0x40, 0x02}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0b, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.StepResponse); + + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).number); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).timestamp); + Assert.assertEquals(2, ((FitnessData.MessageData.StepResponse) packet).containers.size()); + + Assert.assertEquals(0x00, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x20, 0x01}, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).data); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).bitmap); + Assert.assertEquals(0x20, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).distance); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.get(0).bitmap); + Assert.assertEquals(0x20, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.get(0).value); + + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x40, 0x02}, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).data); + Assert.assertEquals(0x3e, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.get(0).bitmap); + Assert.assertEquals(0x40, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.get(0).tag); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).distance); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.size()); +// Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.get(0).bitmap); +// Assert.assertEquals(0x40, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.get(0).tag); +// Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.get(0).value); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testActivityReminderRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException, HuaweiCrypto.CryptoException { + boolean longSitSwitch = false; + byte longSitInterval = 0x00; + byte[] longSitStart = {0x01, 0x02}; + byte[] longSitEnd = {0x03, 0x04}; + byte cycle = 0x05; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, longSitSwitch) + .put(0x03, longSitInterval) + .put(0x04, longSitStart) + .put(0x05, longSitEnd) + .put(0x06, cycle) + ); + expectedTlv.encrypt(secretsProvider); + + byte[] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x07, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0x5b, (byte) 0x9b, (byte) 0x16, (byte) 0xa8, (byte) 0x65, (byte) 0x81, (byte) 0xc1, (byte) 0x18, (byte) 0x2f, (byte) 0x42, (byte) 0xab, (byte) 0xf3, (byte) 0x43, (byte) 0x1e, (byte) 0x5c, (byte) 0x32, (byte) 0x9a, (byte) 0xa9, (byte) 0xa2, (byte) 0x18, (byte) 0x36, (byte) 0xb3, (byte) 0x60, (byte) 0x39, (byte) 0xeb, (byte) 0xdb, (byte) 0x6b, (byte) 0xe5, (byte) 0xac, (byte) 0x7b, (byte) 0x45, (byte) 0x36, (byte) 0xbc, (byte) 0x0c}; + + FitnessData.ActivityReminder.Request request = new FitnessData.ActivityReminder.Request( + secretsProvider, + longSitSwitch, + longSitInterval, + longSitStart, + longSitEnd, + cycle + ); + + Assert.assertEquals(0x07, request.serviceId); + Assert.assertEquals(0x07, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testTruSleepRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiCrypto.CryptoException, HuaweiPacket.CryptoException { + boolean truSleepSwitch = false; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, truSleepSwitch); + expectedTlv.encrypt(secretsProvider); + + byte [] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x16, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x02, (byte) 0x23}; + + FitnessData.TruSleep.Request request = new FitnessData.TruSleep.Request(secretsProvider, truSleepSwitch); + + Assert.assertEquals(0x07, request.serviceId); + Assert.assertEquals(0x16, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testMessageDataStepResponseNoCount() throws NoSuchFieldException, IllegalAccessException { + // I've seen this happening because of a bug in the counts, so it's probably best to stop the sync if this happens. + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x4c, (byte) 0xdd, (byte) 0x99, (byte) 0x79, (byte) 0xf6, (byte) 0x3c, (byte) 0x1e, (byte) 0xbb, (byte) 0x0a, (byte) 0x95, (byte) 0x8d, (byte) 0x12, (byte) 0x05, (byte) 0x81, (byte) 0x7f, (byte) 0xff, (byte) 0xeb, (byte) 0x45}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + try { + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + Assert.fail(); + } catch (HuaweiPacket.ParseException e) { + if (e instanceof HuaweiPacket.MissingTagException) { + Assert.assertNotNull(e.getMessage()); + if (!e.getMessage().equals("Missing tag: 2")) { + Assert.fail(); + } + } else { + Assert.fail(); + } + } + } + + @Test + public void testSpoData() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0x30, (byte) 0xdf, (byte) 0x42, (byte) 0xc9, (byte) 0x79, (byte) 0x91, (byte) 0x36, (byte) 0x3d, (byte) 0x80, (byte) 0x6b, (byte) 0x99, (byte) 0xd3, (byte) 0x3f, (byte) 0xbf, (byte) 0x1f, (byte) 0x1e, (byte) 0xc1, (byte) 0x0b, (byte) 0xbf, (byte) 0xcd, (byte) 0xae, (byte) 0x38, (byte) 0x89, (byte) 0x60, (byte) 0x60, (byte) 0xf7, (byte) 0x93, (byte) 0x84, (byte) 0x3a, (byte) 0x09, (byte) 0xd3, (byte) 0x77, (byte) 0x1e, (byte) 0xb9}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x03, 0xCAFEBEEF) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x00) + .put(0x06, new byte[] {(byte) 0x80, 0x01, 0x42}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0b, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.StepResponse); + + Assert.assertEquals(0x1337, ((FitnessData.MessageData.StepResponse) packet).number); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.size()); + + Assert.assertEquals(0x00, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestampOffset); + Assert.assertArrayEquals(new byte[] {(byte) 0x80, 0x01, 0x42}, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).data); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.size()); + Assert.assertEquals(2, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).bitmap); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).tag); + Assert.assertEquals(0x42, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedDataError); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.size()); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestLocaleConfig.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestLocaleConfig.java new file mode 100644 index 000000000..c628c3fe4 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestLocaleConfig.java @@ -0,0 +1,87 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestLocaleConfig { + + HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testSetLocaleRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, new byte[] {0x45, 0x4e, 0x2d, 0x47, 0x42}) + .put(0x02, (byte) 0x00); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0c, (byte) 0x01, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x4e, (byte) 0xb0, (byte) 0x71, (byte) 0x05, (byte) 0x7b, (byte) 0xf1, (byte) 0x07, (byte) 0x31, (byte) 0xc4, (byte) 0x6c, (byte) 0x5b, (byte) 0x6d, (byte) 0xbf, (byte) 0x07, (byte) 0xf5, (byte) 0x55, (byte) 0x65, (byte) 0x06}; + + LocaleConfig.SetLanguageSetting request = new LocaleConfig.SetLanguageSetting( + paramsProvider, + new byte[] {0x45, 0x4e, 0x2d, 0x47, 0x42}, + (byte) 0x00 + ); + + Assert.assertEquals(0x0c, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestMusicControl.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestMusicControl.java new file mode 100644 index 000000000..7704bc4d7 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestMusicControl.java @@ -0,0 +1,338 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl.Control.Response.Button; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestMusicControl { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testMusicStatusRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int okInput = 0x000186a0; + int errInput = 0x00000000; + + byte commandId1 = 0x01; + byte commandId2 = 0x02; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV okExpectedTlv = new HuaweiTLV() + .put(0x7f, okInput); + HuaweiTLV errExpectedTlv = new HuaweiTLV() + .put(0x7f, errInput); + + MusicControl.MusicStatusRequest okRequest = new MusicControl.MusicStatusRequest(secretsProvider, commandId1, okInput); + MusicControl.MusicStatusRequest errRequest = new MusicControl.MusicStatusRequest(secretsProvider, commandId2, errInput); + + Assert.assertEquals(0x25, okRequest.serviceId); + Assert.assertEquals(commandId1, okRequest.commandId); + Assert.assertEquals(okExpectedTlv, tlvField.get(okRequest)); + Assert.assertTrue(okRequest.complete); + + // To check it doesn't error + okRequest.serialize(); + + Assert.assertEquals(0x25, errRequest.serviceId); + Assert.assertEquals(commandId2, errRequest.commandId); + Assert.assertEquals(errExpectedTlv, tlvField.get(errRequest)); + Assert.assertTrue(errRequest.complete); + + // To check it doesn't error + errRequest.serialize(); + } + + @Test + public void testMusicStatusResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x25, (byte) 0x01, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x01, (byte) 0x43, (byte) 0xdb, (byte) 0x63, (byte) 0xee, (byte) 0x66, (byte) 0xb0, (byte) 0xcd, (byte) 0xff, (byte) 0x9f, (byte) 0x69, (byte) 0x91, (byte) 0x76, (byte) 0x80, (byte) 0x15, (byte) 0x1e, (byte) 0x52, (byte) 0x46}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV(); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x25, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet instanceof MusicControl.MusicStatusResponse); + + // TODO: complete test when more is known about packet contents + } + + @Test + public void testMusicStatusResponseUnencrypted() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawOk = new byte[] {0x5a, 0x00, 0x09, 0x00, 0x25, 0x01, 0x7f, 0x04, 0x00, 0x01, (byte) 0x86, (byte) 0xa0, 0x63, (byte) 0x96}; + byte[] rawErr = new byte[] {0x5a, 0x00, 0x09, 0x00, 0x25, 0x01, 0x7f, 0x04, 0x00, 0x01, (byte) 0x86, (byte) 0xaa, (byte) 0xc2, (byte) 0xdc}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvOk = new HuaweiTLV().put(0x7F, 0x000186A0); + HuaweiTLV expectedTlvErr = new HuaweiTLV().put(0x7F, 0x000186AA); + + HuaweiPacket packetOk = new HuaweiPacket(secretsProvider).parse(rawOk); + HuaweiPacket packetErr = new HuaweiPacket(secretsProvider).parse(rawErr); + packetOk.parseTlv(); + packetErr.parseTlv(); + + Assert.assertEquals(0x25, packetOk.serviceId); + Assert.assertEquals(0x01, packetOk.commandId); + Assert.assertEquals(expectedTlvOk, tlvField.get(packetOk)); + Assert.assertTrue(packetOk instanceof MusicControl.MusicStatusResponse); + Assert.assertEquals(0x000186A0, ((MusicControl.MusicStatusResponse) packetOk).status); + + Assert.assertEquals(0x25, packetErr.serviceId); + Assert.assertEquals(0x01, packetErr.commandId); + Assert.assertEquals(expectedTlvErr, tlvField.get(packetErr)); + Assert.assertTrue(packetErr instanceof MusicControl.MusicStatusResponse); + Assert.assertEquals(0x000186AA, ((MusicControl.MusicStatusResponse) packetErr).status); + } + + @Test + public void testMusicInfoRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + String artistName = "Artist"; + String songName = "Song"; + byte playState = 0x01; + byte maxVolume = 0x03; + byte currentVolume = 0x02; + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, artistName) + .put(0x02, songName) + .put(0x03, playState) + .put(0x04, maxVolume) + .put(0x05, currentVolume); + byte[] expectedSerializedPacket = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0x21, (byte) 0x8b, (byte) 0xe1, (byte) 0x3d, (byte) 0x9f, (byte) 0x85, (byte) 0xd2, (byte) 0x2e, (byte) 0x64, (byte) 0x87, (byte) 0x3f, (byte) 0x1d, (byte) 0xab, (byte) 0x3f, (byte) 0xc7, (byte) 0x39, (byte) 0xb6, (byte) 0x34, (byte) 0x89, (byte) 0x60, (byte) 0xa0, (byte) 0x36, (byte) 0x4a, (byte) 0x08, (byte) 0x7a, (byte) 0x16, (byte) 0xed, (byte) 0xc9, (byte) 0x9e, (byte) 0xf3, (byte) 0xbf, (byte) 0x44, (byte) 0xac, (byte) 0x58}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + MusicControl.MusicInfo.Request musicInfoRequest = new MusicControl.MusicInfo.Request( + secretsProvider, + artistName, + songName, + playState, + maxVolume, + currentVolume + ); + + Assert.assertEquals(0x25, musicInfoRequest.serviceId); + Assert.assertEquals(0x02, musicInfoRequest.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(musicInfoRequest)); + Assert.assertTrue(musicInfoRequest.complete); + List out = musicInfoRequest.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expectedSerializedPacket, out.get(0)); + } + + @Test + public void testMusicInfoResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawOk = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0x7f, (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x86, (byte) 0xA0, (byte) 0xbb, (byte) 0x14}; + byte[] rawErr = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0x7f, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x88, (byte) 0xf0}; + byte[] rawMissing = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0xb4, (byte) 0x1b}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV okExpectedTlv = new HuaweiTLV() + .put(0x7f, 0x000186a0); + HuaweiTLV errExpectedTlv = new HuaweiTLV() + .put(0x7f, 0x00000000); + HuaweiTLV missingExpectedTlv = new HuaweiTLV(); + + HuaweiPacket packetOk = new HuaweiPacket(secretsProvider).parse(rawOk); + HuaweiPacket packetErr = new HuaweiPacket(secretsProvider).parse(rawErr); + HuaweiPacket packetMissing = new HuaweiPacket(secretsProvider).parse(rawMissing); + + packetOk.parseTlv(); + packetErr.parseTlv(); + packetMissing.parseTlv(); + + Assert.assertEquals(0x25, packetOk.serviceId); + Assert.assertEquals(0x02, packetOk.commandId); + Assert.assertEquals(okExpectedTlv, tlvField.get(packetOk)); + Assert.assertTrue(packetOk instanceof MusicControl.MusicInfo.Response); + Assert.assertTrue(((MusicControl.MusicInfo.Response) packetOk).ok); + Assert.assertEquals("", ((MusicControl.MusicInfo.Response) packetOk).error); + + Assert.assertEquals(0x25, packetErr.serviceId); + Assert.assertEquals(0x02, packetErr.commandId); + Assert.assertEquals(errExpectedTlv, tlvField.get(packetErr)); + Assert.assertTrue(packetErr instanceof MusicControl.MusicInfo.Response); + Assert.assertFalse(((MusicControl.MusicInfo.Response) packetErr).ok); + Assert.assertEquals("Music information error code: 0", ((MusicControl.MusicInfo.Response) packetErr).error); + + Assert.assertEquals(0x25, packetMissing.serviceId); + Assert.assertEquals(0x02, packetMissing.commandId); + Assert.assertEquals(missingExpectedTlv, tlvField.get(packetMissing)); + Assert.assertTrue(packetMissing instanceof MusicControl.MusicInfo.Response); + Assert.assertFalse(((MusicControl.MusicInfo.Response) packetMissing).ok); + Assert.assertEquals("Music information response no status tag", ((MusicControl.MusicInfo.Response) packetMissing).error); + } + + @Test + public void testControlResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] emptyInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0xa4, (byte) 0x3a, }; + byte[] playPauseInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0xe6, (byte) 0x85}; + byte[] previousInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0xc6, (byte) 0xc7}; + byte[] nextInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x04, (byte) 0xb6, (byte) 0x20}; + byte[] unknownButtonInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0xFF, (byte) 0xe8, (byte) 0x54}; + byte[] volumeInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x02, (byte) 0x01, (byte) 0x42, (byte) 0xc7, (byte) 0x72}; + byte[] combinedInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x02, (byte) 0x01, (byte) 0x42, (byte) 0x95, (byte) 0x9a}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV emptyExpectedTlv = new HuaweiTLV(); + HuaweiTLV playPauseExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x01); + HuaweiTLV previousExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x03); + HuaweiTLV nextExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x04); + HuaweiTLV unknownButtonExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0xFF); + HuaweiTLV volumeExpectedTlv = new HuaweiTLV() + .put(0x02, (byte) 0x42); + HuaweiTLV combinedExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x01) + .put(0x02, (byte) 0x42); + + HuaweiPacket emptyResponse = new HuaweiPacket(secretsProvider).parse(emptyInput); + HuaweiPacket playPauseResponse = new HuaweiPacket(secretsProvider).parse(playPauseInput); + HuaweiPacket previousResponse = new HuaweiPacket(secretsProvider).parse(previousInput); + HuaweiPacket nextResponse = new HuaweiPacket(secretsProvider).parse(nextInput); + HuaweiPacket unknownButtonResponse = new HuaweiPacket(secretsProvider).parse(unknownButtonInput); + HuaweiPacket volumeResponse = new HuaweiPacket(secretsProvider).parse(volumeInput); + HuaweiPacket combinedResponse = new HuaweiPacket(secretsProvider).parse(combinedInput); + + emptyResponse.parseTlv(); + playPauseResponse.parseTlv(); + previousResponse.parseTlv(); + nextResponse.parseTlv(); + unknownButtonResponse.parseTlv(); + volumeResponse.parseTlv(); + combinedResponse.parseTlv(); + + // TODO: play and pause are now split - test needs to be updated + + Assert.assertEquals(0x25, emptyResponse.serviceId); + Assert.assertEquals(0x03, emptyResponse.commandId); + Assert.assertEquals(emptyExpectedTlv, tlvField.get(emptyResponse)); + Assert.assertTrue(emptyResponse instanceof MusicControl.Control.Response); + Assert.assertFalse(((MusicControl.Control.Response) emptyResponse).buttonPresent); + Assert.assertFalse(((MusicControl.Control.Response) emptyResponse).volumePresent); + + Assert.assertEquals(0x25, playPauseResponse.serviceId); + Assert.assertEquals(0x03, playPauseResponse.commandId); + Assert.assertEquals(playPauseExpectedTlv, tlvField.get(playPauseResponse)); + Assert.assertTrue(playPauseResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) playPauseResponse).buttonPresent); + Assert.assertEquals(0x01, ((MusicControl.Control.Response) playPauseResponse).rawButton); + // Assert.assertEquals(Button.PlayPause, ((MusicControl.Control.Response) playPauseResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) playPauseResponse).volumePresent); + + Assert.assertEquals(0x25, previousResponse.serviceId); + Assert.assertEquals(0x03, previousResponse.commandId); + Assert.assertEquals(previousExpectedTlv, tlvField.get(previousResponse)); + Assert.assertTrue(previousResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) previousResponse).buttonPresent); + Assert.assertEquals(0x03, ((MusicControl.Control.Response) previousResponse).rawButton); + Assert.assertEquals(Button.Previous, ((MusicControl.Control.Response) previousResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) previousResponse).volumePresent); + + Assert.assertEquals(0x25, nextResponse.serviceId); + Assert.assertEquals(0x03, nextResponse.commandId); + Assert.assertEquals(nextExpectedTlv, tlvField.get(nextResponse)); + Assert.assertTrue(nextResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) nextResponse).buttonPresent); + Assert.assertEquals(0x04, ((MusicControl.Control.Response) nextResponse).rawButton); + Assert.assertEquals(Button.Next, ((MusicControl.Control.Response) nextResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) nextResponse).volumePresent); + + Assert.assertEquals(0x25, unknownButtonResponse.serviceId); + Assert.assertEquals(0x03, unknownButtonResponse.commandId); + Assert.assertEquals(unknownButtonExpectedTlv, tlvField.get(unknownButtonResponse)); + Assert.assertTrue(unknownButtonResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) unknownButtonResponse).buttonPresent); + Assert.assertEquals((byte) 0xFF, ((MusicControl.Control.Response) unknownButtonResponse).rawButton); + Assert.assertEquals(Button.Unknown, ((MusicControl.Control.Response) unknownButtonResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) unknownButtonResponse).volumePresent); + + Assert.assertEquals(0x25, volumeResponse.serviceId); + Assert.assertEquals(0x03, volumeResponse.commandId); + Assert.assertEquals(volumeExpectedTlv, tlvField.get(volumeResponse)); + Assert.assertTrue(volumeResponse instanceof MusicControl.Control.Response); + Assert.assertFalse(((MusicControl.Control.Response) volumeResponse).buttonPresent); + Assert.assertTrue(((MusicControl.Control.Response) volumeResponse).volumePresent); + Assert.assertEquals(0x42, ((MusicControl.Control.Response) volumeResponse).volume); + + Assert.assertEquals(0x25, combinedResponse.serviceId); + Assert.assertEquals(0x03, combinedResponse.commandId); + Assert.assertEquals(combinedExpectedTlv, tlvField.get(combinedResponse)); + Assert.assertTrue(combinedResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) combinedResponse).buttonPresent); + Assert.assertEquals(0x01, ((MusicControl.Control.Response) combinedResponse).rawButton); + // Assert.assertEquals(Button.PlayPause, ((MusicControl.Control.Response) combinedResponse).button); + Assert.assertTrue(((MusicControl.Control.Response) combinedResponse).volumePresent); + Assert.assertEquals(0x42, ((MusicControl.Control.Response) combinedResponse).volume); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestNotifications.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestNotifications.java new file mode 100644 index 000000000..52e1a3087 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestNotifications.java @@ -0,0 +1,197 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestNotifications { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testNotificationActionRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short notificationId = 0x01; + byte notificationType = 0x02; + byte titleEncoding = 0x02; + String titleContent = "Title"; + byte senderEncoding = 0x02; + String senderContent = "Sender"; + byte bodyEncoding = 0x02; + String bodyContent = "Body"; + String sourceAppId = "SourceApp"; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, notificationId) + .put(0x02, notificationType) + .put(0x03, true) + .put(0x84, new HuaweiTLV() + .put(0x8C, new HuaweiTLV() + .put(0x8D, new HuaweiTLV() + .put(0x0E, (byte) 0x03) + .put(0x0F, titleEncoding) + .put(0x10, titleContent) + ) + .put(0x8D, new HuaweiTLV() + .put(0x0E, (byte) 0x02) + .put(0x0F, senderEncoding) + .put(0x10, senderContent) + ) + .put(0x8D, new HuaweiTLV() + .put(0x0E, (byte) 0x01) + .put(0x0F, bodyEncoding) + .put(0x10, bodyContent) + ) + ) + ) + .put(0x11, sourceAppId); + + Notifications.NotificationActionRequest request = new Notifications.NotificationActionRequest( + secretsProvider, + notificationId, + notificationType, + titleEncoding, + titleContent, + senderEncoding, + senderContent, + bodyEncoding, + bodyContent, + sourceAppId + ); + + Assert.assertEquals(0x02, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + Assert.assertTrue(request.complete); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + + // Only check that this doesn't error + request.serialize(); + } + + @Test + public void testSetNotificationRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvTrue = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, true) + .put(0x03, true) + ); + + HuaweiTLV expectedTlvFalse = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, false) + .put(0x03, false) + ); + + byte[] expectedOutputTrue = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x04, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xd9, (byte) 0xc4, (byte) 0xaa, (byte) 0x7d, (byte) 0xa3, (byte) 0x5c, (byte) 0x42, (byte) 0xab, (byte) 0x2d, (byte) 0xc2, (byte) 0xe7, (byte) 0x73, (byte) 0xc0, (byte) 0x4c, (byte) 0x97, (byte) 0x5a, (byte) 0x41, (byte) 0x23}; + byte[] expectedOutputFalse = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x04, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xeb, (byte) 0x1f, (byte) 0x20, (byte) 0x0a, (byte) 0x7d, (byte) 0xe2, (byte) 0x25, (byte) 0x45, (byte) 0x01, (byte) 0x5b, (byte) 0xe8, (byte) 0x24, (byte) 0xe3, (byte) 0x7e, (byte) 0x1d, (byte) 0x9c, (byte) 0x47, (byte) 0x31}; + + Notifications.NotificationStateRequest requestTrue = new Notifications.NotificationStateRequest(secretsProvider, true); + Notifications.NotificationStateRequest requestFalse = new Notifications.NotificationStateRequest(secretsProvider, false); + + Assert.assertEquals(0x02, requestTrue.serviceId); + Assert.assertEquals(0x04, requestTrue.commandId); + Assert.assertTrue(requestTrue.complete); + Assert.assertEquals(expectedTlvTrue, tlvField.get(requestTrue)); + List outTrue = requestTrue.serialize(); + Assert.assertEquals(1, outTrue.size()); + Assert.assertArrayEquals(expectedOutputTrue, outTrue.get(0)); + + Assert.assertEquals(0x02, requestFalse.serviceId); + Assert.assertEquals(0x04, requestFalse.commandId); + Assert.assertTrue(requestFalse.complete); + Assert.assertEquals(expectedTlvFalse, tlvField.get(requestFalse)); + List outFalse = requestFalse.serialize(); + Assert.assertEquals(1, outFalse.size()); + Assert.assertArrayEquals(expectedOutputFalse, outFalse.get(0)); + } + + @Test + public void testSetWearMessagePushRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvTrue = new HuaweiTLV() + .put(0x01, true); + + HuaweiTLV expectedTlvFalse = new HuaweiTLV() + .put(0x01, false); + + byte[] expectedOutputTrue = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xcd, (byte) 0x97, (byte) 0x7e, (byte) 0x01, (byte) 0x48, (byte) 0x34, (byte) 0x2a, (byte) 0x48, (byte) 0x58, (byte) 0x0d, (byte) 0x30, (byte) 0xc7, (byte) 0xbc, (byte) 0x2e, (byte) 0x40, (byte) 0xd4, (byte) 0x29, (byte) 0xe0}; + byte[] expectedOutputFalse = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x30, (byte) 0xd2}; + + Notifications.WearMessagePushRequest requestTrue = new Notifications.WearMessagePushRequest(secretsProvider, true); + Notifications.WearMessagePushRequest requestFalse = new Notifications.WearMessagePushRequest(secretsProvider, false); + + Assert.assertEquals(0x02, requestTrue.serviceId); + Assert.assertEquals(0x08, requestTrue.commandId); + Assert.assertTrue(requestTrue.complete); + Assert.assertEquals(expectedTlvTrue, tlvField.get(requestTrue)); + List outTrue = requestTrue.serialize(); + Assert.assertEquals(1, outTrue.size()); + Assert.assertArrayEquals(expectedOutputTrue, outTrue.get(0)); + + Assert.assertEquals(0x02, requestFalse.serviceId); + Assert.assertEquals(0x08, requestFalse.commandId); + Assert.assertTrue(requestFalse.complete); + Assert.assertEquals(expectedTlvFalse, tlvField.get(requestFalse)); + List outFalse = requestFalse.serialize(); + Assert.assertEquals(1, outFalse.size()); + Assert.assertArrayEquals(expectedOutputFalse, outFalse.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkMode.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkMode.java new file mode 100644 index 000000000..a2c4eabad --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkMode.java @@ -0,0 +1,94 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestWorkMode { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testSwitchStatusRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvTrue = new HuaweiTLV() + .put(0x01, true); + HuaweiTLV expectedTlvFalse = new HuaweiTLV() + .put(0x01, false); + + byte[] serializedTrue = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x26, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xcd, (byte) 0x97, (byte) 0x7e, (byte) 0x01, (byte) 0x48, (byte) 0x34, (byte) 0x2a, (byte) 0x48, (byte) 0x58, (byte) 0x0d, (byte) 0x30, (byte) 0xc7, (byte) 0xbc, (byte) 0x2e, (byte) 0x40, (byte) 0xd4, (byte) 0x5c, (byte) 0x5a}; + byte[] serializedFalse = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x26, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x45, (byte) 0x68}; + + WorkMode.SwitchStatusRequest requestTrue = new WorkMode.SwitchStatusRequest(secretsProvider, true); + WorkMode.SwitchStatusRequest requestFalse = new WorkMode.SwitchStatusRequest(secretsProvider, false); + + Assert.assertEquals(0x26, requestTrue.serviceId); + Assert.assertEquals(0x02, requestTrue.commandId); + Assert.assertEquals(expectedTlvTrue, tlvField.get(requestTrue)); + Assert.assertTrue(requestTrue.complete); + List outTrue = requestTrue.serialize(); + Assert.assertEquals(1, outTrue.size()); + Assert.assertArrayEquals(serializedTrue, outTrue.get(0)); + + Assert.assertEquals(0x26, requestFalse.serviceId); + Assert.assertEquals(0x02, requestFalse.commandId); + Assert.assertEquals(expectedTlvFalse, tlvField.get(requestFalse)); + Assert.assertTrue(requestFalse.complete); + List outFalse = requestFalse.serialize(); + Assert.assertEquals(1, outFalse.size()); + Assert.assertArrayEquals(serializedFalse, outFalse.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkout.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkout.java new file mode 100644 index 000000000..a81671ce7 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkout.java @@ -0,0 +1,437 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestWorkout { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getAuthMode() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testWorkoutCountRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int start = 0x00000000; + int end = 0x01020304; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x03, start) + .put(0x04, end) + ); + + byte[] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x07, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf7, (byte) 0x48, (byte) 0xf7, (byte) 0x49, (byte) 0x4a, (byte) 0xa5, (byte) 0xb2, (byte) 0xc9, (byte) 0x41, (byte) 0xf5, (byte) 0x7f, (byte) 0xb4, (byte) 0xe9, (byte) 0x17, (byte) 0xac, (byte) 0xb5, (byte) 0x5f, (byte) 0x8e}; + + Workout.WorkoutCount.Request request = new Workout.WorkoutCount.Request(secretsProvider, start, end); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x07, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutCountResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x4a, (byte) 0x00, (byte) 0x17, (byte) 0x07, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x30, (byte) 0xee, (byte) 0xdd, (byte) 0xa9, (byte) 0x23, (byte) 0x2c, (byte) 0xe4, (byte) 0x9f, (byte) 0x41, (byte) 0x0b, (byte) 0x9f, (byte) 0x7a, (byte) 0xc2, (byte) 0xe0, (byte) 0x72, (byte) 0x6d, (byte) 0xe1, (byte) 0x8f, (byte) 0xd0, (byte) 0xe7, (byte) 0x41, (byte) 0x59, (byte) 0x38, (byte) 0xac, (byte) 0x17, (byte) 0x66, (byte) 0xc8, (byte) 0x60, (byte) 0xd7, (byte) 0xd2, (byte) 0x32, (byte) 0x8b, (byte) 0xa5, (byte) 0x91, (byte) 0xc7, (byte) 0xc5, (byte) 0xe5, (byte) 0x7d, (byte) 0x8d, (byte) 0xa1, (byte) 0xd0, (byte) 0x6f, (byte) 0xe2, (byte) 0xe2, (byte) 0x24, (byte) 0x7d, (byte) 0xef, (byte) 0x02, (byte) 0x03, (byte) 0x59, (byte) 0x3e}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x85, new HuaweiTLV() + .put(0x06, (short) 0x0001) + .put(0x07, (short) 0x0002) + .put(0x08, (short) 0x0003) + .put(0x0a, (short) 0x0004) + ) + .put(0x85, new HuaweiTLV() + .put(0x06, (short) 0x0005) + .put(0x07, (short) 0x0006) + .put(0x08, (short) 0x0007) + .put(0x0a, (short) 0x0008) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x07, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutCount.Response); + Assert.assertEquals(0x1337, ((Workout.WorkoutCount.Response) packet).count); + Assert.assertEquals(2, ((Workout.WorkoutCount.Response) packet).workoutNumbers.size()); + + Assert.assertArrayEquals(new byte[] {0x06, 0x02, 0x00, 0x01, 0x07, 0x02, 0x00, 0x02, 0x08, 0x02, 0x00, 0x03, 0x0a, 0x02, 0x00, 0x04}, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).rawData); + Assert.assertEquals(0x01, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).workoutNumber); + Assert.assertEquals(0x02, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).dataCount); + Assert.assertEquals(0x03, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).paceCount); + + Assert.assertArrayEquals(new byte[] {0x06, 0x02, 0x00, 0x05, 0x07, 0x02, 0x00, 0x06, 0x08, 0x02, 0x00, 0x07, 0x0a, 0x02, 0x00, 0x08}, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).rawData); + Assert.assertEquals(0x05, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).workoutNumber); + Assert.assertEquals(0x06, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).dataCount); + Assert.assertEquals(0x07, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).paceCount); + } + + @Test + public void testWorkoutTotalsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short number = 0x1337; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, number) + ); + + byte[] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x26, (byte) 0x91}; + + Workout.WorkoutTotals.Request request = new Workout.WorkoutTotals.Request(secretsProvider, number); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x08, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutTotalsResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x17, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0x0f, (byte) 0xa0, (byte) 0x3a, (byte) 0x90, (byte) 0xae, (byte) 0x8c, (byte) 0xcf, (byte) 0x03, (byte) 0xce, (byte) 0x5a, (byte) 0x68, (byte) 0x87, (byte) 0x05, (byte) 0x51, (byte) 0xf7, (byte) 0x2f, (byte) 0x78, (byte) 0xbd, (byte) 0x84, (byte) 0xf1, (byte) 0x4f, (byte) 0xb8, (byte) 0x51, (byte) 0x28, (byte) 0xec, (byte) 0xfd, (byte) 0x8b, (byte) 0x2e, (byte) 0x99, (byte) 0xd3, (byte) 0x42, (byte) 0xd7, (byte) 0x65, (byte) 0xb2, (byte) 0x82, (byte) 0x02, (byte) 0x28, (byte) 0x00, (byte) 0x34, (byte) 0xbc, (byte) 0x39, (byte) 0x59, (byte) 0x8f, (byte) 0x0b, (byte) 0xa7, (byte) 0x3a, (byte) 0x5c, (byte) 0xfb, (byte) 0xf1, (byte) 0xd4, (byte) 0x8f, (byte) 0xf6, (byte) 0x6d, (byte) 0x98, (byte) 0xd6, (byte) 0x5a, (byte) 0x51, (byte) 0x0a, (byte) 0x4a, (byte) 0x1c, (byte) 0x42, (byte) 0xc8, (byte) 0x9d, (byte) 0xee, (byte) 0x55, (byte) 0x44}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x03, (byte) 0x01) + .put(0x04, 0x01020304) + .put(0x05, 0x05060708) + .put(0x06, 0x090a0b0c) + .put(0x07, 0x0d0e0f10) + .put(0x08, 0x11121314) + .put(0x09, 0x15161718) + .put(0x12, 0x191a1b1c) + .put(0x14, (byte) 0x1d) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + // TODO: find out what the status and type can be + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x08, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutTotals.Response); + Assert.assertArrayEquals(new byte[] {0x02, 0x02, 0x13, 0x37, 0x03, 0x01, 0x01, 0x04, 0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x08, 0x06, 0x04, 0x09, 0x0a, 0x0b, 0x0c, 0x07, 0x04, 0x0d, 0x0e, 0x0f, 0x10, 0x08, 0x04, 0x11, 0x12, 0x13, 0x14, 0x09, 0x04, 0x15, 0x16, 0x17, 0x18, 0x12, 0x04, 0x19, 0x1a, 0x1b, 0x1c, 0x14, 0x01, 0x1d}, ((Workout.WorkoutTotals.Response) packet).rawData); + Assert.assertEquals(0x1337, ((Workout.WorkoutTotals.Response) packet).number); + Assert.assertEquals(0x01, ((Workout.WorkoutTotals.Response) packet).status); + Assert.assertEquals(0x01020304, ((Workout.WorkoutTotals.Response) packet).startTime); + Assert.assertEquals(0x05060708, ((Workout.WorkoutTotals.Response) packet).endTime); + Assert.assertEquals(0x090a0b0c, ((Workout.WorkoutTotals.Response) packet).calories); + Assert.assertEquals(0x0d0e0f10, ((Workout.WorkoutTotals.Response) packet).distance); + Assert.assertEquals(0x11121314, ((Workout.WorkoutTotals.Response) packet).stepCount); + Assert.assertEquals(0x15161718, ((Workout.WorkoutTotals.Response) packet).totalTime); + Assert.assertEquals(0x191a1b1c, ((Workout.WorkoutTotals.Response) packet).duration); + Assert.assertEquals(0x1d, ((Workout.WorkoutTotals.Response) packet).type); + } + + @Test + public void testWorkoutDataRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short workoutNumber = 0x0102; + short dataNumber = 0x0304; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x03, dataNumber) + ); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xd2, (byte) 0xd7, (byte) 0x55, (byte) 0x23, (byte) 0xeb, (byte) 0x51, (byte) 0x4f, (byte) 0xe0, (byte) 0x35, (byte) 0x6c, (byte) 0x60, (byte) 0xc5, (byte) 0xbf, (byte) 0x61, (byte) 0x68, (byte) 0xd1, (byte) 0x03, (byte) 0x83}; + + Workout.WorkoutData.Request request = new Workout.WorkoutData.Request(secretsProvider, workoutNumber, dataNumber); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x0a, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutDataResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x17, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0x03, (byte) 0x66, (byte) 0xf5, (byte) 0x16, (byte) 0xc9, (byte) 0x60, (byte) 0xb9, (byte) 0xf2, (byte) 0xe3, (byte) 0x88, (byte) 0x99, (byte) 0xab, (byte) 0x50, (byte) 0x22, (byte) 0xcb, (byte) 0x83, (byte) 0x53, (byte) 0xd0, (byte) 0xb2, (byte) 0xc3, (byte) 0x66, (byte) 0xa9, (byte) 0x16, (byte) 0x23, (byte) 0xa5, (byte) 0x8e, (byte) 0x81, (byte) 0x68, (byte) 0x85, (byte) 0x38, (byte) 0x3e, (byte) 0xd5, (byte) 0x8e, (byte) 0x21, (byte) 0xc8, (byte) 0xa1, (byte) 0x80, (byte) 0x98, (byte) 0x2d, (byte) 0x78, (byte) 0x75, (byte) 0x80, (byte) 0xa1, (byte) 0x39, (byte) 0x61, (byte) 0xa6, (byte) 0x3e, (byte) 0x61, (byte) 0x2c, (byte) 0x5e, (byte) 0xe2, (byte) 0x6f, (byte) 0xef, (byte) 0xdf, (byte) 0xdb, (byte) 0x39, (byte) 0x8f, (byte) 0xab, (byte) 0x21, (byte) 0xde, (byte) 0xba, (byte) 0xdb, (byte) 0x2c, (byte) 0xff, (byte) 0x97, (byte) 0x94}; + + short workoutNumber = 0x0102; + short dataNumber = 0x0304; + + int timestamp = 0x05060708; + byte interval = 0x09; + short dataCount = 0x0002; + byte dataLength = 0x0F; // Data length must match + short bitmap = 0x0042; // Inner data and speed + + short speed1 = 0x0a0b; + short cadence1 = 0x0c0d; + short stepLength1 = 0x0e0f; + short groundContactTime1 = 0x1011; + byte groundImpact1 = 0x12; + short swingAngle1 = 0x1314; + byte foreFootLanding1 = 0x15; + byte midFootLanding1 = 0x16; + byte backFootLanding1 = 0x17; + byte eversionAngle1 = 0x18; + + short speed2 = 0x191a; + short cadence2 = 0x1b1c; + short stepLength2 = 0x1d1e; + short groundContactTime2 = 0x1f20; + byte groundImpact2 = 0x21; + short swingAngle2 = 0x2223; + byte foreFootLanding2 = 0x24; + byte midFootLanding2 = 0x25; + byte backFootLanding2 = 0x26; + byte eversionAngle2 = 0x27; + + ByteBuffer headerBuf = ByteBuffer.allocate(14); + headerBuf.putShort(workoutNumber); + headerBuf.putShort(dataNumber); + headerBuf.putInt(timestamp); + headerBuf.put(interval); + headerBuf.putShort(dataCount); + headerBuf.put(dataLength); + headerBuf.putShort(bitmap); + + ByteBuffer dataBuf = ByteBuffer.allocate(30); + + dataBuf.putShort(speed1); + dataBuf.putShort(cadence1); + dataBuf.putShort(stepLength1); + dataBuf.putShort(groundContactTime1); + dataBuf.put(groundImpact1); + dataBuf.putShort(swingAngle1); + dataBuf.put(foreFootLanding1); + dataBuf.put(midFootLanding1); + dataBuf.put(backFootLanding1); + dataBuf.put(eversionAngle1); + + dataBuf.putShort(speed2); + dataBuf.putShort(cadence2); + dataBuf.putShort(stepLength2); + dataBuf.putShort(groundContactTime2); + dataBuf.put(groundImpact2); + dataBuf.putShort(swingAngle2); + dataBuf.put(foreFootLanding2); + dataBuf.put(midFootLanding2); + dataBuf.put(backFootLanding2); + dataBuf.put(eversionAngle2); + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x03, dataNumber) + .put(0x04, headerBuf.array()) + .put(0x05, dataBuf.array()) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x0a, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutData.Response); + + Assert.assertEquals(0x0102, ((Workout.WorkoutData.Response) packet).workoutNumber); + Assert.assertEquals(0x0304, ((Workout.WorkoutData.Response) packet).dataNumber); + Assert.assertArrayEquals(headerBuf.array(), ((Workout.WorkoutData.Response) packet).rawHeader); + Assert.assertArrayEquals(dataBuf.array(), ((Workout.WorkoutData.Response) packet).rawData); + + Assert.assertEquals(0x0102, ((Workout.WorkoutData.Response) packet).header.workoutNumber); + Assert.assertEquals(0x0304, ((Workout.WorkoutData.Response) packet).header.dataNumber); + Assert.assertEquals(0x05060708, ((Workout.WorkoutData.Response) packet).header.timestamp); + Assert.assertEquals(0x09, ((Workout.WorkoutData.Response) packet).header.interval); + Assert.assertEquals(0x0002, ((Workout.WorkoutData.Response) packet).header.dataCount); + Assert.assertEquals(0x0f, ((Workout.WorkoutData.Response) packet).header.dataLength); + Assert.assertEquals(0x0042, ((Workout.WorkoutData.Response) packet).header.bitmap); + + Assert.assertEquals(2, ((Workout.WorkoutData.Response) packet).dataList.size()); + + Assert.assertNull(((Workout.WorkoutData.Response) packet).dataList.get(0).unknownData); + Assert.assertEquals(0x0a0b, ((Workout.WorkoutData.Response) packet).dataList.get(0).speed); + Assert.assertEquals(0x0c0d, ((Workout.WorkoutData.Response) packet).dataList.get(0).cadence); + Assert.assertEquals(0x0e0f, ((Workout.WorkoutData.Response) packet).dataList.get(0).stepLength); + Assert.assertEquals(0x1011, ((Workout.WorkoutData.Response) packet).dataList.get(0).groundContactTime); + Assert.assertEquals(0x12, ((Workout.WorkoutData.Response) packet).dataList.get(0).impact); + Assert.assertEquals(0x1314, ((Workout.WorkoutData.Response) packet).dataList.get(0).swingAngle); + Assert.assertEquals(0x15, ((Workout.WorkoutData.Response) packet).dataList.get(0).foreFootLanding); + Assert.assertEquals(0x16, ((Workout.WorkoutData.Response) packet).dataList.get(0).midFootLanding); + Assert.assertEquals(0x17, ((Workout.WorkoutData.Response) packet).dataList.get(0).backFootLanding); + Assert.assertEquals(0x18, ((Workout.WorkoutData.Response) packet).dataList.get(0).eversionAngle); + + Assert.assertNull(((Workout.WorkoutData.Response) packet).dataList.get(1).unknownData); + Assert.assertEquals(0x191a, ((Workout.WorkoutData.Response) packet).dataList.get(1).speed); + Assert.assertEquals(0x1b1c, ((Workout.WorkoutData.Response) packet).dataList.get(1).cadence); + Assert.assertEquals(0x1d1e, ((Workout.WorkoutData.Response) packet).dataList.get(1).stepLength); + Assert.assertEquals(0x1f20, ((Workout.WorkoutData.Response) packet).dataList.get(1).groundContactTime); + Assert.assertEquals(0x21, ((Workout.WorkoutData.Response) packet).dataList.get(1).impact); + Assert.assertEquals(0x2223, ((Workout.WorkoutData.Response) packet).dataList.get(1).swingAngle); + Assert.assertEquals(0x24, ((Workout.WorkoutData.Response) packet).dataList.get(1).foreFootLanding); + Assert.assertEquals(0x25, ((Workout.WorkoutData.Response) packet).dataList.get(1).midFootLanding); + Assert.assertEquals(0x26, ((Workout.WorkoutData.Response) packet).dataList.get(1).backFootLanding); + Assert.assertEquals(0x27, ((Workout.WorkoutData.Response) packet).dataList.get(1).eversionAngle); + } + + @Test + public void testWorkoutPaceRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short workoutNumber = 0x0102; + short paceNumber = 0x0304; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x08, paceNumber) + ); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x0c, (byte) 0x18, (byte) 0x24, (byte) 0x67, (byte) 0x5e, (byte) 0xe9, (byte) 0x8d, (byte) 0x36, (byte) 0x5f, (byte) 0xde, (byte) 0x1c, (byte) 0x9e, (byte) 0xa0, (byte) 0xd7, (byte) 0x0a, (byte) 0x01, (byte) 0xd3, (byte) 0xce}; + + Workout.WorkoutPace.Request request = new Workout.WorkoutPace.Request(secretsProvider, workoutNumber, paceNumber); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x0c, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutPaceResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = {(byte) 0x5a, (byte) 0x00, (byte) 0x4a, (byte) 0x00, (byte) 0x17, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x30, (byte) 0xe8, (byte) 0xfe, (byte) 0xb9, (byte) 0x27, (byte) 0xa6, (byte) 0xc5, (byte) 0x81, (byte) 0x65, (byte) 0x51, (byte) 0xb8, (byte) 0x24, (byte) 0xfe, (byte) 0x2a, (byte) 0xdc, (byte) 0x3d, (byte) 0x22, (byte) 0xd7, (byte) 0x34, (byte) 0x62, (byte) 0xaf, (byte) 0x06, (byte) 0x5f, (byte) 0xfe, (byte) 0x9c, (byte) 0xe8, (byte) 0xa6, (byte) 0x87, (byte) 0x23, (byte) 0xd6, (byte) 0xc7, (byte) 0x7a, (byte) 0xeb, (byte) 0x07, (byte) 0x06, (byte) 0x5c, (byte) 0x35, (byte) 0xe8, (byte) 0x99, (byte) 0xd3, (byte) 0x96, (byte) 0x0b, (byte) 0x99, (byte) 0x38, (byte) 0x65, (byte) 0x48, (byte) 0xcf, (byte) 0x0f, (byte) 0x99, (byte) 0xe2, (byte) 0x23}; + + short workoutNumber = 0x0102; + short paceNumber = 0x0304; + + short distance1 = 0x0506; + byte type1 = 0x07; + int pace1 = 0x08090a0b; + + short distance2 = 0x0c0d; + byte type2 = 0x0e; + int pace2 = 0x0f101112; + short correction = 0x1314; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x08, paceNumber) + .put(0x83, new HuaweiTLV() + .put(0x04, distance1) + .put(0x05, type1) + .put(0x06, pace1) + ) + .put(0x83, new HuaweiTLV() + .put(0x04, distance2) + .put(0x05, type2) + .put(0x06, pace2) + .put(0x09, correction) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x0c, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutPace.Response); + Assert.assertEquals(0x0102, ((Workout.WorkoutPace.Response) packet).workoutNumber); + Assert.assertEquals(0x0304, ((Workout.WorkoutPace.Response) packet).paceNumber); + Assert.assertEquals(2, ((Workout.WorkoutPace.Response) packet).blocks.size()); + + Assert.assertEquals(0x0506, ((Workout.WorkoutPace.Response) packet).blocks.get(0).distance); + Assert.assertEquals(0x07, ((Workout.WorkoutPace.Response) packet).blocks.get(0).type); + Assert.assertEquals(0x08090a0b, ((Workout.WorkoutPace.Response) packet).blocks.get(0).pace); + Assert.assertEquals(0, ((Workout.WorkoutPace.Response) packet).blocks.get(0).correction); + + Assert.assertEquals(0x0c0d, ((Workout.WorkoutPace.Response) packet).blocks.get(1).distance); + Assert.assertEquals(0x0e, ((Workout.WorkoutPace.Response) packet).blocks.get(1).type); + Assert.assertEquals(0x0f101112, ((Workout.WorkoutPace.Response) packet).blocks.get(1).pace); + Assert.assertEquals(0x1314, ((Workout.WorkoutPace.Response) packet).blocks.get(1).correction); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java new file mode 100644 index 000000000..b93a546b7 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java @@ -0,0 +1,396 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.Transaction; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.DebugRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; + +public class TestDebugRequestParser { + + HuaweiSupportProvider supportProvider = new HuaweiSupportProvider(new HuaweiLESupport()) { + + @Override + public boolean isBLE() { + return true; + } + + @Override + public Context getContext() { + return null; + } + + @Override + public GBDevice getDevice() { + return null; + } + + @Override + public byte[] getSerial() { + return new byte[0]; + } + + @Override + public String getDeviceMac() { + return null; + } + + @Override + public byte[] getMacAddress() { + return new byte[0]; + } + + @Override + public byte[] getAndroidId() { + return new byte[0]; + } + + @Override + public short getNotificationId() { + return 0; + } + + @Override + public TransactionBuilder createBrTransactionBuilder(String taskName) { + return null; + } + + @Override + public nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder createLeTransactionBuilder(String taskName) { + return null; + } + + @Override + public void performConnected(Transaction transaction) throws IOException { + + } + + @Override + public void performConnected(nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction transaction) throws IOException { + + } + + @Override + public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) { + + } + + @Override + public BluetoothGattCharacteristic getLeCharacteristic(UUID uuid) { + return null; + } + + @Override + public HuaweiPacket.ParamsProvider getParamsProvider() { + return null; + } + + @Override + public void addInProgressRequest(Request request) { + + } + + @Override + public void removeInProgressRequests(Request request) { + + } + + @Override + public void setSecretKey(byte[] authKey) { + + } + + @Override + public byte[] getSecretKey() { + return new byte[0]; + } + + @Override + public void addTotalFitnessData(int steps, int calories, int distance) { + + } + + @Override + public void addSleepActivity(int timestamp, short duration, byte type) { + + } + + @Override + public void addStepData(int timestamp, short steps, short calories, short distance, byte spo, byte heartrate) { + + } + + @Override + public Long addWorkoutTotalsData(Workout.WorkoutTotals.Response packet) { + return null; + } + + @Override + public void addWorkoutSampleData(Long workoutId, List dataList) { + + } + + @Override + public void addWorkoutPaceData(Long workoutId, List paceList) { + + } + + @Override + public void sendSetMusic() { + + } + }; + + @Test + public void emptyPacket() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void emptyTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,/)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void byteTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, (byte) 1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,B1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void shortTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, (short) 1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,S1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void integerTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, (int) 1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,I1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void booleanTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, true)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,b1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void arrayTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, new byte[] {(byte) 0xCA, (byte) 0xFE})); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,aCAFE)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void stringTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, new byte[] {0x79, 0x65, 0x73})); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,-yes)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void hexValues() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, new byte[] {0x79, 0x65, 0x73})); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("0x01,0x1,false,(0x01,-yes)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void largeServiceCommand() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = (byte) 0xff; + expected.commandId = (byte) 255; + expected.setEncryption(false); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("0xff,255,false"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void subTlv() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(1) + .put(2) + )); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(129,(1,/),(2,/))"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void subSubSubTlv() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(1) + ) + ) + )); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(129,(129,(129,(1,/))))"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void subTlvVCombined() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(1) + .put(2, true) + ) + .put(1, true) + ); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(129,(1,/),(2,b1)),(1,b1)"); + + Assert.assertEquals(expected, packet); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java new file mode 100644 index 000000000..f5d3982de --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java @@ -0,0 +1,449 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.Transaction; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; + +@RunWith(MockitoJUnitRunner.class) +public class TestResponseManager { + + HuaweiSupportProvider supportProvider = new HuaweiSupportProvider(new HuaweiLESupport()) { + + @Override + public boolean isBLE() { + return true; + } + + @Override + public Context getContext() { + return null; + } + + @Override + public GBDevice getDevice() { + return null; + } + + @Override + public byte[] getSerial() { + return new byte[0]; + } + + @Override + public String getDeviceMac() { + return null; + } + + @Override + public byte[] getMacAddress() { + return new byte[0]; + } + + @Override + public byte[] getAndroidId() { + return new byte[0]; + } + + @Override + public short getNotificationId() { + return 0; + } + + @Override + public TransactionBuilder createBrTransactionBuilder(String taskName) { + return null; + } + + @Override + public nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder createLeTransactionBuilder(String taskName) { + return null; + } + + @Override + public void performConnected(Transaction transaction) throws IOException { + + } + + @Override + public void performConnected(nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction transaction) throws IOException { + + } + + @Override + public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) { + + } + + @Override + public BluetoothGattCharacteristic getLeCharacteristic(UUID uuid) { + return null; + } + + @Override + public HuaweiPacket.ParamsProvider getParamsProvider() { + return null; + } + + @Override + public void addInProgressRequest(Request request) { + + } + + @Override + public void removeInProgressRequests(Request request) { + + } + + @Override + public void setSecretKey(byte[] authKey) { + + } + + @Override + public byte[] getSecretKey() { + return new byte[0]; + } + + @Override + public void addTotalFitnessData(int steps, int calories, int distance) { + + } + + @Override + public void addSleepActivity(int timestamp, short duration, byte type) { + + } + + @Override + public void addStepData(int timestamp, short steps, short calories, short distance, byte spo, byte heartrate) { + + } + + @Override + public Long addWorkoutTotalsData(Workout.WorkoutTotals.Response packet) { + return null; + } + + @Override + public void addWorkoutSampleData(Long workoutId, List dataList) { + + } + + @Override + public void addWorkoutPaceData(Long workoutId, List paceList) { + + } + + @Override + public void sendSetMusic() { + + } + }; + + Field handlersField; + Field receivedPacketField; + Field asynchronousResponseField; + + @Before + public void beforeClass() throws NoSuchFieldException { + handlersField = ResponseManager.class.getDeclaredField("handlers"); + handlersField.setAccessible(true); + + asynchronousResponseField = ResponseManager.class.getDeclaredField("asynchronousResponse"); + asynchronousResponseField.setAccessible(true); + + receivedPacketField = ResponseManager.class.getDeclaredField("receivedPacket"); + receivedPacketField.setAccessible(true); + } + + @Test + public void testAddHandler() throws IllegalAccessException { + Request input = new Request(supportProvider); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(input); + + ResponseManager responseManager = new ResponseManager(supportProvider); + responseManager.addHandler(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + } + + @Test + public void testRemoveHandler() throws IllegalAccessException { + Request input = new Request(supportProvider); + Request extra = new Request(supportProvider); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(extra); + inputHandlers.add(input); + inputHandlers.add(extra); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(extra); + expectedHandlers.add(extra); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + + responseManager.removeHandler(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + } + + @Test + public void testHandleDataCompletePacketSynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input = {0x01, 0x02, 0x03, 0x04}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = true; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(true); + Request request2 = Mockito.mock(Request.class); + when(request2.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(1)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + } + + @Test + public void testHandleDataCompletePacketAsynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input = {0x01, 0x02, 0x03, 0x04}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = true; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + Request request2 = Mockito.mock(Request.class); + when(request2.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(request1); + expectedHandlers.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input); + verify(mockAsynchronousResponse, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(1)).handleResponse(mockHuaweiPacket); + verify(request2, times(0)).handleResponse(); + } + + @Test + public void testHandleDataTwoPartialPacketsSynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input1 = {0x01, 0x02, 0x03, 0x04}; + byte[] input2 = {0x05, 0x06, 0x07, 0x08}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = false; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(true); + Request request2 = Mockito.mock(Request.class); + when(request2.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers1 = Collections.synchronizedList(new ArrayList()); + expectedHandlers1.add(request1); + expectedHandlers1.add(request2); + + List expectedHandlers2 = Collections.synchronizedList(new ArrayList()); + expectedHandlers2.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input1); + + Assert.assertEquals(expectedHandlers1, handlersField.get(responseManager)); + Assert.assertEquals(mockHuaweiPacket, receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input1); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(0)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + + mockHuaweiPacket.complete = true; + responseManager.handleData(input2); + + Assert.assertEquals(expectedHandlers2, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input2); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(1)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + } + + @Test + public void testHandleDataTwoPartialPacketsAsynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input1 = {0x01, 0x02, 0x03, 0x04}; + byte[] input2 = {0x05, 0x06, 0x07, 0x08}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = false; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + Request request2 = Mockito.mock(Request.class); + when(request2.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(request1); + expectedHandlers.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input1); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertEquals(mockHuaweiPacket, receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input1); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(0)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + + mockHuaweiPacket.complete = true; + responseManager.handleData(input2); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input2); + verify(mockAsynchronousResponse, times(1)).handleResponse((HuaweiPacket) any()); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(1)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + } +}