From a3bd7a297a814e487a3c07eefb141b06e537b96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 16 Feb 2024 22:56:27 +0000 Subject: [PATCH] Test Device: Add fake features and data --- .../devices/test/TestDeviceConst.java | 25 + .../devices/test/TestDeviceCoordinator.java | 633 +++++++++++++++--- .../devices/test/TestDeviceRand.java | 47 ++ .../TestDeviceSpecificSettingsCustomizer.java | 84 +++ .../devices/test/TestFeature.java | 66 ++ .../activity/TestActivitySummaryParser.java | 27 + .../test/samples/TestPaiSampleProvider.java | 163 +++++ .../test/samples/TestSampleProvider.java | 241 +++++++ .../test/samples/TestSpo2SampleProvider.java | 105 +++ .../samples/TestStressSampleProvider.java | 105 +++ .../TestTemperatureSampleProvider.java | 92 +++ .../devices/test/TestDeviceSupport.java | 81 +++ app/src/main/res/values/strings.xml | 2 + .../res/xml/devicesettings_test_features.xml | 12 + 14 files changed, 1608 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceConst.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceRand.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceSpecificSettingsCustomizer.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestFeature.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestPaiSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSpo2SampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestStressSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestTemperatureSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/test/TestDeviceSupport.java create mode 100644 app/src/main/res/xml/devicesettings_test_features.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceConst.java new file mode 100644 index 000000000..f9a3d6e35 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceConst.java @@ -0,0 +1,25 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test; + +public class TestDeviceConst { + public static final String PREF_TEST_FEATURES = "test_features"; + + private TestDeviceConst() { + // utility class + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceCoordinator.java index 8b1ab96c0..450525490 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceCoordinator.java @@ -23,113 +23,596 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.apache.commons.lang3.ArrayUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability; +import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl; +import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.RoidmiConst; +import nodomain.freeyourgadget.gadgetbridge.devices.test.activity.TestActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.devices.test.samples.TestPaiSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.samples.TestSpo2SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.samples.TestStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.samples.TestSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.samples.TestTemperatureSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; +import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; +import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.unknown.UnknownDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.test.TestDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class TestDeviceCoordinator extends AbstractDeviceCoordinator { @Override - protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { - - } - - @NonNull - @Override - public boolean supports(GBDeviceCandidate candidate) { - return false; - } - - @Nullable - @Override - public Class getPairingActivity() { - return null; + protected void deleteDevice(@NonNull final GBDevice gbDevice, + @NonNull final Device device, + @NonNull final DaoSession session) throws GBException { + // Nothing to do } @Override - public boolean supportsActivityDataFetching() { - return false; - } - - @Override - public boolean supportsActivityTracking() { - return false; - } - - @Override - public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { - return null; - } - - @Override - public InstallHandler findInstallHandler(Uri uri, Context context) { - return null; - } - - @Override - public boolean supportsScreenshots() { - return false; - } - - @Override - public int getAlarmSlotCount(GBDevice device) { - return 0; - } - - @Override - public boolean supportsSmartWakeup(GBDevice device) { + public boolean supports(final GBDeviceCandidate candidate) { return false; } @Override public String getManufacturer() { - return "Test"; - } - - @Override - public boolean supportsAppsManagement(GBDevice device) { - return false; - } - - @Override - public Class getAppsManagementActivity() { - return null; - } - - @Override - public boolean supportsCalendarEvents() { - return false; - } - - @Override - public boolean supportsRealtimeData() { - return false; - } - - @Override - public boolean supportsFindDevice() { - return false; + return "Gadgetbridge"; } @NonNull @Override public Class getDeviceSupportClass() { - return UnknownDeviceSupport.class; + return TestDeviceSupport.class; } - - @Override public int getDeviceNameResource() { return R.string.devicetype_test; } + + @Override + public SampleProvider getSampleProvider(final GBDevice device, final DaoSession session) { + return supportsActivityTracking() ? new TestSampleProvider(device, session) : super.getSampleProvider(device, session); + } + + @Override + public TimeSampleProvider getStressSampleProvider(final GBDevice device, final DaoSession session) { + return supportsStressMeasurement() ? new TestStressSampleProvider() : super.getStressSampleProvider(device, session); + } + + @Override + public int[] getStressRanges() { + // TODO getStressRanges + return super.getStressRanges(); + } + + @Override + public TimeSampleProvider getTemperatureSampleProvider(final GBDevice device, final DaoSession session) { + return new TestTemperatureSampleProvider(); // TODO supportsTemperature + } + + @Override + public TimeSampleProvider getSpo2SampleProvider(final GBDevice device, final DaoSession session) { + return supportsSpo2() ? new TestSpo2SampleProvider() : super.getSpo2SampleProvider(device, session); + } + + @Override + public TimeSampleProvider getHeartRateMaxSampleProvider(final GBDevice device, final DaoSession session) { + // TODO getHeartRateMaxSampleProvider + return super.getHeartRateMaxSampleProvider(device, session); + } + + @Override + public TimeSampleProvider getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) { + // TODO getHeartRateRestingSampleProvider + return super.getHeartRateRestingSampleProvider(device, session); + } + + @Override + public TimeSampleProvider getHeartRateManualSampleProvider(final GBDevice device, final DaoSession session) { + // TODO getHeartRateManualSampleProvider + return super.getHeartRateManualSampleProvider(device, session); + } + + @Override + public TimeSampleProvider getPaiSampleProvider(final GBDevice device, final DaoSession session) { + return supportsPai() ? new TestPaiSampleProvider() : super.getPaiSampleProvider(device, session); + } + + @Override + public TimeSampleProvider getSleepRespiratoryRateSampleProvider(final GBDevice device, final DaoSession session) { + // TODO getHeartRateManualSampleProvider + return super.getSleepRespiratoryRateSampleProvider(device, session); + } + + @Nullable + @Override + public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { + return supportsActivityTracks() ? new TestActivitySummaryParser() : super.getActivitySummaryParser(device); + } + + @Override + public File getAppCacheDir() throws IOException { + return new File(FileUtils.getExternalFilesDir(), "test-app-cache"); + } + + @Override + public String getAppCacheSortFilename() { + return "test-app-cache-order.txt"; + } + + @Override + public String getAppFileExtension() { + return ".txt"; + } + + @Override + public boolean supportsAppListFetching() { + return supports(getTestDevice(), TestFeature.APP_LIST_FETCHING); + } + + @Override + public boolean supportsFlashing() { + return supports(getTestDevice(), TestFeature.FLASHING); + } + + @Nullable + @Override + public InstallHandler findInstallHandler(final Uri uri, final Context context) { + // TODO findInstallHandler? + return super.findInstallHandler(uri, context); + } + + @Override + public boolean supportsScreenshots() { + return supports(getTestDevice(), TestFeature.SCREENSHOTS); + } + + @Override + public int getAlarmSlotCount(final GBDevice device) { + return super.getAlarmSlotCount(device); + } + + @Override + public boolean supportsSmartWakeup(final GBDevice device) { + return supports(getTestDevice(), TestFeature.SMART_WAKEUP); + } + + @Override + public boolean supportsAppReordering() { + return supports(getTestDevice(), TestFeature.APP_REORDERING); + } + + @Override + public boolean supportsAppsManagement(final GBDevice device) { + return supports(getTestDevice(), TestFeature.APPS_MANAGEMENT); + } + + @Override + public boolean supportsCachedAppManagement(final GBDevice device) { + return supports(getTestDevice(), TestFeature.CACHED_APP_MANAGEMENT); + } + + @Override + public boolean supportsInstalledAppManagement(final GBDevice device) { + return supports(getTestDevice(), TestFeature.INSTALLED_APP_MANAGEMENT); + } + + @Override + public boolean supportsWatchfaceManagement(final GBDevice device) { + return supports(getTestDevice(), TestFeature.WATCHFACE_MANAGEMENT); + } + + @Nullable + @Override + public Class getAppsManagementActivity() { + return AppManagerActivity.class; + } + + @Nullable + @Override + public Class getWatchfaceDesignerActivity() { + // TODO getWatchfaceDesignerActivity + return super.getWatchfaceDesignerActivity(); + } + + @Override + public boolean supportsCalendarEvents() { + return supports(getTestDevice(), TestFeature.CALENDAR_EVENTS); + } + + @Override + public boolean supportsActivityDataFetching() { + return supports(getTestDevice(), TestFeature.ACTIVITY_DATA_FETCHING); + } + + @Override + public boolean supportsActivityTracking() { + return supports(getTestDevice(), TestFeature.ACTIVITY_TRACKING); + } + + @Override + public boolean supportsActivityTracks() { + return supports(getTestDevice(), TestFeature.ACTIVITY_TRACKS); + } + + @Override + public boolean supportsStressMeasurement() { + return supports(getTestDevice(), TestFeature.STRESS_MEASUREMENT); + } + + @Override + public boolean supportsSpo2() { + return supports(getTestDevice(), TestFeature.SPO2); + } + + @Override + public boolean supportsHeartRateStats() { + return supports(getTestDevice(), TestFeature.HEART_RATE_STATS); + } + + @Override + public boolean supportsPai() { + return supports(getTestDevice(), TestFeature.PAI); + } + + @Override + public int getPaiName() { + return R.string.menuitem_pai; + } + + @Override + public boolean supportsPaiTime() { + return supports(getTestDevice(), TestFeature.PAI_TIME); + } + + @Override + public boolean supportsSleepRespiratoryRate() { + return supports(getTestDevice(), TestFeature.SLEEP_RESPIRATORY_RATE); + } + + @Override + public boolean supportsAlarmSnoozing() { + return supports(getTestDevice(), TestFeature.ALARM_SNOOZING); + } + + @Override + public boolean supportsAlarmTitle(final GBDevice device) { + return supports(getTestDevice(), TestFeature.ALARM_TITLE); + } + + @Override + public int getAlarmTitleLimit(final GBDevice device) { + return supports(getTestDevice(), TestFeature.ALARM_TITLE_LIMIT) ? 10 : -1; + } + + @Override + public boolean supportsAlarmDescription(final GBDevice device) { + return supports(getTestDevice(), TestFeature.ALARM_DESCRIPTION); + } + + @Override + public boolean supportsMusicInfo() { + return supports(getTestDevice(), TestFeature.MUSIC_INFO); + } + + @Override + public boolean supportsLedColor() { + return supports(getTestDevice(), TestFeature.LED_COLOR); + } + + @Override + public int getMaximumReminderMessageLength() { + // TODO getMaximumReminderMessageLength + return 16; + } + + @Override + public int getReminderSlotCount(final GBDevice device) { + return supports(getTestDevice(), TestFeature.REMINDERS) ? 3 : 0; + } + + @Override + public int getCannedRepliesSlotCount(final GBDevice device) { + return supports(getTestDevice(), TestFeature.CANNED_REPLIES) ? 3 : 0; + } + + @Override + public int getWorldClocksSlotCount() { + return supports(getTestDevice(), TestFeature.WORLD_CLOCKS) ? 3 : 0; + } + + @Override + public int getWorldClocksLabelLength() { + // TODO getWorldClocksLabelLength + return 10; + } + + @Override + public boolean supportsDisabledWorldClocks() { + return supports(getTestDevice(), TestFeature.DISABLED_WORLD_CLOCKS); + } + + @Override + public int getContactsSlotCount(final GBDevice device) { + return supports(getTestDevice(), TestFeature.CONTACTS) ? 3 : 0; + } + + @Override + public boolean supportsRgbLedColor() { + return supports(getTestDevice(), TestFeature.RGB_LED_COLOR); + } + + @NonNull + @Override + public int[] getColorPresets() { + return RoidmiConst.COLOR_PRESETS; + } + + @Override + public boolean supportsHeartRateMeasurement(final GBDevice device) { + return supports(getTestDevice(), TestFeature.HEART_RATE_MEASUREMENT); + } + + @Override + public boolean supportsManualHeartRateMeasurement(final GBDevice device) { + return supports(getTestDevice(), TestFeature.MANUAL_HEART_RATE_MEASUREMENT); + } + + @Override + public boolean supportsRealtimeData() { + return supports(getTestDevice(), TestFeature.REALTIME_DATA); + } + + @Override + public boolean supportsRemSleep() { + return supports(getTestDevice(), TestFeature.REM_SLEEP); + } + + @Override + public boolean supportsWeather() { + return supports(getTestDevice(), TestFeature.WEATHER); + } + + @Override + public boolean supportsFindDevice() { + return supports(getTestDevice(), TestFeature.FIND_DEVICE); + } + + @Override + public boolean supportsUnicodeEmojis() { + return supports(getTestDevice(), TestFeature.UNICODE_EMOJIS); + } + + @Override + public int[] getSupportedDeviceSpecificConnectionSettings() { + return super.getSupportedDeviceSpecificConnectionSettings(); + } + + @Override + public int[] getSupportedDeviceSpecificApplicationSettings() { + return new int[]{ + R.xml.devicesettings_custom_deviceicon, + }; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(final GBDevice device) { + final List settings = new ArrayList<>(); + + settings.add(R.xml.devicesettings_header_apps); + settings.add(R.xml.devicesettings_loyalty_cards); + + if (getWorldClocksSlotCount() > 0) { + settings.add(R.xml.devicesettings_header_time); + settings.add(R.xml.devicesettings_world_clocks); + } + + if (getContactsSlotCount(device) > 0) { + settings.add(R.xml.devicesettings_header_other); + settings.add(R.xml.devicesettings_contacts); + } + + settings.add(R.xml.devicesettings_header_developer); + settings.add(R.xml.devicesettings_test_features); + + return ArrayUtils.toPrimitive(settings.toArray(new Integer[0])); + } + + @Override + public int[] getSupportedDeviceSpecificAuthenticationSettings() { + return new int[]{R.xml.devicesettings_pairingkey}; + } + + @Override + public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { + return new TestDeviceSpecificSettingsCustomizer(); + } + + @Override + public String[] getSupportedLanguageSettings(final GBDevice device) { + return new String[]{ + "auto", + "en_US", + }; + } + + @Nullable + @Override + public Class getCalibrationActivity() { + // TODO getCalibrationActivity + return super.getCalibrationActivity(); + } + + @Override + public int getBatteryCount() { + return supports(getTestDevice(), TestFeature.BATTERIES_MULTIPLE) ? 3 : 1; + } + + @Override + public BatteryConfig[] getBatteryConfig() { + if (getBatteryCount() == 1) { + return super.getBatteryConfig(); + } + + final BatteryConfig[] ret = new BatteryConfig[getBatteryCount()]; + + for (int i = 0; i < getBatteryCount(); i++) { + ret[i] = new BatteryConfig(i, R.drawable.ic_battery_full, R.string.battery); + } + + return ret; + } + + @Override + public boolean supportsPowerOff() { + return supports(getTestDevice(), TestFeature.POWER_OFF); + } + + @Override + public PasswordCapabilityImpl.Mode getPasswordCapability() { + return super.getPasswordCapability(); + } + + @Override + public List getHeartRateMeasurementIntervals() { + return super.getHeartRateMeasurementIntervals(); + } + + @Override + public boolean supportsWidgets(final GBDevice device) { + return supports(getTestDevice(), TestFeature.WIDGETS); + } + + @Nullable + @Override + public WidgetManager getWidgetManager(final GBDevice device) { + return super.getWidgetManager(device); + } + + @Override + public boolean supportsNavigation() { + return supports(getTestDevice(), TestFeature.NAVIGATION); + } + + @Override + public EnumSet getInitialFlags() { + return EnumSet.noneOf(ServiceDeviceSupport.Flags.class); + } + + @Override + public int getDefaultIconResource() { + // TODO getDefaultIconResource + return super.getDefaultIconResource(); + } + + @Override + public int getDisabledIconResource() { + // TODO getDisabledIconResource + return super.getDisabledIconResource(); + } + + @Override + public boolean supportsNotificationVibrationPatterns() { + return supports(getTestDevice(), TestFeature.NOTIFICATION_VIBRATION_PATTERNS); + } + + @Override + public boolean supportsNotificationVibrationRepetitionPatterns() { + return supports(getTestDevice(), TestFeature.NOTIFICATION_VIBRATION_REPETITION_PATTERNS); + } + + @Override + public boolean supportsNotificationLedPatterns() { + return supports(getTestDevice(), TestFeature.NOTIFICATION_LED_PATTERNS); + } + + @Override + public AbstractNotificationPattern[] getNotificationVibrationPatterns() { + // TODO getNotificationVibrationPatterns + return new AbstractNotificationPattern[0]; + } + + @Override + public AbstractNotificationPattern[] getNotificationVibrationRepetitionPatterns() { + // TODO getNotificationVibrationRepetitionPatterns + return new AbstractNotificationPattern[0]; + } + + @Override + public AbstractNotificationPattern[] getNotificationLedPatterns() { + // TODO getNotificationLedPatterns + return new AbstractNotificationPattern[0]; + } + + @Override + public boolean validateAuthKey(final String authKey) { + return authKey.equals("hunter2"); + } + + public boolean supports(final GBDevice device, final TestFeature feature) { + if (device == null) { + return false; + } + + final Set features = new HashSet<>(getPrefs(device).getStringSet(TestDeviceConst.PREF_TEST_FEATURES, Collections.emptySet())); + // if nothing enable, enable everything + return features.isEmpty() || features.contains(feature.name()); + } + + @Nullable + private GBDevice getTestDevice() { + // HACK: Avoid the refactor of adding a device as a parameter on all functions + // This means that it's only possible to have a single test device + + if (GBApplication.app() == null || GBApplication.app().getDeviceManager() == null) { + return null; + } + + final List devices = GBApplication.app().getDeviceManager().getDevices(); + for (GBDevice device : devices) { + if (DeviceType.TEST == device.getType()) { + return device; + } + } + + return null; + } + + protected static Prefs getPrefs(final GBDevice device) { + return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress())); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceRand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceRand.java new file mode 100644 index 000000000..e04d429e1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceRand.java @@ -0,0 +1,47 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test; + +import java.util.Random; + +/** + * Utility class to try and get deterministic random values across multiple users. This is not efficient + * and should definitely be improved. + */ +public class TestDeviceRand { + public static final long BASE_TIMESTAMP = 1420499943000L; + + private TestDeviceRand() { + // utility class + } + + public static int randInt(final long ts, final int min, final int max) { + return new Random(ts - BASE_TIMESTAMP).nextInt((max - min) + 1) + min; + } + + public static long randLong(final long ts, final long min, final long max) { + return (long) (new Random(ts - BASE_TIMESTAMP).nextFloat() * (max - min) + min); + } + + public static float randFloat(final long ts, final float min, final float max) { + return new Random(ts - BASE_TIMESTAMP).nextFloat() * (max - min) + min; + } + + public static boolean randBool(final long ts, final float pTrue) { + return new Random(ts - BASE_TIMESTAMP).nextFloat() < pTrue; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceSpecificSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceSpecificSettingsCustomizer.java new file mode 100644 index 000000000..c17d59300 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestDeviceSpecificSettingsCustomizer.java @@ -0,0 +1,84 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test; + +import android.os.Parcel; + +import androidx.preference.MultiSelectListPreference; +import androidx.preference.Preference; + +import java.util.Collections; +import java.util.Set; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class TestDeviceSpecificSettingsCustomizer implements DeviceSpecificSettingsCustomizer { + @Override + public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) { + if (TestDeviceConst.PREF_TEST_FEATURES.equals(preference.getKey())) { + handler.getDevice().sendDeviceUpdateIntent(handler.getContext()); + } + } + + @Override + public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) { + final Preference pref = handler.findPreference(TestDeviceConst.PREF_TEST_FEATURES); + if (pref == null) { + return; + } + + // Populate the preference directly from the enum + final CharSequence[] entries = new CharSequence[TestFeature.values().length]; + final CharSequence[] values = new CharSequence[TestFeature.values().length]; + for (int i = 0; i < TestFeature.values().length; i++) { + entries[i] = TestFeature.values()[i].name(); + values[i] = TestFeature.values()[i].name(); + } + if (pref instanceof MultiSelectListPreference) { + ((MultiSelectListPreference) pref).setEntries(entries); + ((MultiSelectListPreference) pref).setEntryValues(values); + } + } + + @Override + public Set getPreferenceKeysWithSummary() { + return Collections.emptySet(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public TestDeviceSpecificSettingsCustomizer createFromParcel(final Parcel in) { + return new TestDeviceSpecificSettingsCustomizer(); + } + + @Override + public TestDeviceSpecificSettingsCustomizer[] newArray(final int size) { + return new TestDeviceSpecificSettingsCustomizer[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestFeature.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestFeature.java new file mode 100644 index 000000000..fbe34697e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/TestFeature.java @@ -0,0 +1,66 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test; + +public enum TestFeature { + ACTIVITY_DATA_FETCHING, + ACTIVITY_TRACKING, + ACTIVITY_TRACKS, + ALARM_DESCRIPTION, + ALARM_SNOOZING, + ALARM_TITLE_LIMIT, + ALARM_TITLE, + APP_LIST_FETCHING, + APP_REORDERING, + APPS_MANAGEMENT, + BATTERIES_MULTIPLE, + CACHED_APP_MANAGEMENT, + CALENDAR_EVENTS, + CANNED_REPLIES, + CONTACTS, + DISABLED_WORLD_CLOCKS, + FIND_DEVICE, + FLASHING, + FM_FREQUENCY, + HEART_RATE_MEASUREMENT, + HEART_RATE_STATS, + INSTALLED_APP_MANAGEMENT, + LED_COLOR, + MANUAL_HEART_RATE_MEASUREMENT, + MUSIC_INFO, + NAVIGATION, + NOTIFICATION_LED_PATTERNS, + NOTIFICATION_VIBRATION_PATTERNS, + NOTIFICATION_VIBRATION_REPETITION_PATTERNS, + PAI_TIME, + PAI, + POWER_OFF, + REALTIME_DATA, + REM_SLEEP, + REMINDERS, + RGB_LED_COLOR, + SCREENSHOTS, + SLEEP_RESPIRATORY_RATE, + SMART_WAKEUP, + SPO2, + STRESS_MEASUREMENT, + UNICODE_EMOJIS, + WATCHFACE_MANAGEMENT, + WEATHER, + WIDGETS, + WORLD_CLOCKS, +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java new file mode 100644 index 000000000..19503354f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java @@ -0,0 +1,27 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test.activity; + +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; + +public class TestActivitySummaryParser implements ActivitySummaryParser { + @Override + public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) { + return summary; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestPaiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestPaiSampleProvider.java new file mode 100644 index 000000000..2eac30e91 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestPaiSampleProvider.java @@ -0,0 +1,163 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test.samples; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceRand; +import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; + +public class TestPaiSampleProvider implements TimeSampleProvider { + @NonNull + @Override + public List getAllSamples(final long timestampFrom, final long timestampTo) { + final List samples = new ArrayList<>(); + + int paiLow = TestDeviceRand.randInt(timestampFrom, 0, 15); + int paiMod = TestDeviceRand.randInt(timestampFrom, 0, 15); + int paiHigh = TestDeviceRand.randInt(timestampFrom, 0, 15); + int paiTotal = paiLow + paiMod + paiHigh; + + for (long ts = timestampFrom; ts < timestampTo; ts += 12 * 60 * 60 * 1000L) { + if (TestDeviceRand.randBool(ts, 0.75f)) { + samples.add(new TestPaiSample( + ts, + paiLow, + paiMod, + paiHigh, + paiTotal + )); + } + paiTotal += paiLow + paiMod + paiHigh; + paiLow += TestDeviceRand.randInt(ts, - paiLow, 15 - paiLow); + paiMod += TestDeviceRand.randInt(ts, - paiMod, 15 - paiMod); + paiHigh += TestDeviceRand.randInt(ts, - paiHigh, 15 - paiHigh); + } + + return samples; + } + + @Override + public void addSample(final PaiSample timeSample) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public void addSamples(final List timeSamples) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public PaiSample createSample() { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Nullable + @Override + public PaiSample getLatestSample() { + final long ts = System.currentTimeMillis(); + return new TestPaiSample( + ts - TestDeviceRand.randLong(ts, 10 * 1000L, 2 * 60 * 60 * 1000L), + TestDeviceRand.randInt(ts, 85, 99), + TestDeviceRand.randInt(ts, 85, 99), + TestDeviceRand.randInt(ts, 85, 99), + TestDeviceRand.randInt(ts, 85, 99) + ); + } + + @Nullable + @Override + public PaiSample getFirstSample() { + return new TestPaiSample( + TestDeviceRand.BASE_TIMESTAMP, + TestDeviceRand.randInt(TestDeviceRand.BASE_TIMESTAMP, 0, 15), + TestDeviceRand.randInt(TestDeviceRand.BASE_TIMESTAMP, 0, 15), + TestDeviceRand.randInt(TestDeviceRand.BASE_TIMESTAMP, 0, 15), + TestDeviceRand.randInt(TestDeviceRand.BASE_TIMESTAMP, 70, 90) + ); + } + + public static class TestPaiSample implements PaiSample { + private final long timestamp; + private final int paiLow; + private final int paiModerate; + private final int paiHigh; + private final int paiTotal; + + public TestPaiSample(final long timestamp, + final int low, + final int moderate, + final int high, + final int total) { + this.timestamp = timestamp; + this.paiLow = low; + this.paiModerate = moderate; + this.paiHigh = high; + this.paiTotal = total; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public float getPaiLow() { + return paiLow; + } + + @Override + public float getPaiModerate() { + return paiModerate; + } + + @Override + public float getPaiHigh() { + return paiHigh; + } + + @Override + public int getTimeLow() { + return paiLow; + } + + @Override + public int getTimeModerate() { + return paiModerate; + } + + @Override + public int getTimeHigh() { + return paiHigh; + } + + @Override + public float getPaiToday() { + return paiLow + paiModerate + paiHigh; + } + + @Override + public float getPaiTotal() { + return paiTotal; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSampleProvider.java new file mode 100644 index 000000000..e23f75328 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSampleProvider.java @@ -0,0 +1,241 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test.samples; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceRand; +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class TestSampleProvider extends AbstractSampleProvider { + public TestSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao getSampleDao() { + return null; + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return null; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return null; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return null; + } + + @Override + public int normalizeType(final int rawType) { + return rawType; + } + + @Override + public int toRawActivityKind(final int activityKind) { + return activityKind; + } + + @Override + public float normalizeIntensity(final int rawIntensity) { + return rawIntensity / 100f; + } + + @Override + public TestActivitySample createActivitySample() { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + protected List getGBActivitySamples(final int timestamp_from, final int timestamp_to, final int activityType) { + final List samples = new ArrayList<>(); + + int[] sleepStages = new int[] { + ActivityKind.TYPE_LIGHT_SLEEP, + ActivityKind.TYPE_DEEP_SLEEP, + }; + if (getDevice().getDeviceCoordinator().supportsRemSleep()) { + sleepStages = ArrayUtils.add(sleepStages, ActivityKind.TYPE_REM_SLEEP); + } + int sleepStageCurrent = 0; + int sleepStageDirection = 1; + int sleepStageDurationRemaining = TestDeviceRand.randInt(timestamp_from * 1000L, 30, 90); + + int steps = TestDeviceRand.randInt(timestamp_from * 1000L, 0, 170); + int intensity = TestDeviceRand.randInt(timestamp_from * 1000L, 0, 100); + int hr = TestDeviceRand.randInt(timestamp_from * 1000L, 90, 153); + + final long bedtimeHour = TestDeviceRand.randInt(timestamp_from, 21, 22); + final long bedtimeMinute = TestDeviceRand.randInt(timestamp_from, 0, 59); + final long wakeHour = TestDeviceRand.randInt(timestamp_from, 6, 9); + final long wakeMinute = TestDeviceRand.randInt(timestamp_from, 0, 59); + + final Calendar cal = GregorianCalendar.getInstance(); + + for (long ts = timestamp_from * 1000L; ts < timestamp_to * 1000L; ts += 60 * 1000L) { + cal.setTimeInMillis(ts); + final int h = cal.get(Calendar.HOUR_OF_DAY); + final int m = cal.get(Calendar.MINUTE); + boolean isSleep = false; + if (h == bedtimeHour) { + isSleep = m >= bedtimeMinute; + } else if (h > bedtimeHour) { + isSleep = true; + } else if (h == wakeHour) { + isSleep = m <= wakeMinute; + } else if (h < wakeHour) { + isSleep = true; + } + + if (TestDeviceRand.randBool(ts, 0.75f)) { + samples.add(new TestActivitySample( + (int) (ts / 1000), + isSleep ? sleepStages[sleepStageCurrent] : ActivityKind.TYPE_UNKNOWN, + steps, + intensity, + hr + )); + } + + if (isSleep) { + sleepStageDurationRemaining--; + if (sleepStageDurationRemaining <= 0) { + sleepStageDurationRemaining = TestDeviceRand.randInt(ts * 1000L, 30, 90); + sleepStageCurrent += sleepStageDirection; + if (sleepStageCurrent == 0 || sleepStageCurrent == sleepStages.length - 1) { + sleepStageDirection *= -1; + } + } + } + + steps += TestDeviceRand.randInt(ts, -steps, 170 - steps); + intensity += TestDeviceRand.randInt(ts, -1, 1); + hr += TestDeviceRand.randInt(ts, -2, 2); + } + + return samples; + } + + public class TestActivitySample extends AbstractActivitySample { + private final int timestamp; + private final int kind; + private final int steps; + private final int intensity; + private final int hr; + + public TestActivitySample(final int timestamp, + final int kind, + final int steps, + final int intensity, + final int hr) { + this.timestamp = timestamp; + this.kind = kind; + this.steps = steps; + this.intensity = intensity; + this.hr = hr; + } + + @Override + public SampleProvider getProvider() { + return TestSampleProvider.this; + } + + @Override + public void setTimestamp(final int timestamp) { + + } + + @Override + public void setUserId(final long userId) { + + } + + @Override + public void setDeviceId(final long deviceId) { + + } + + @Override + public long getDeviceId() { + return 0; + } + + @Override + public long getUserId() { + return 0; + } + + @Override + public int getTimestamp() { + return timestamp; + } + + @Override + public int getKind() { + return kind; + } + + @Override + public int getRawKind() { + return kind; + } + + @Override + public float getIntensity() { + return intensity / 100f; + } + + @Override + public int getHeartRate() { + return hr; + } + + @Override + public int getRawIntensity() { + return intensity; + } + + @Override + public int getSteps() { + return steps; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSpo2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSpo2SampleProvider.java new file mode 100644 index 000000000..ca271fe41 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestSpo2SampleProvider.java @@ -0,0 +1,105 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test.samples; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceRand; +import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; + +public class TestSpo2SampleProvider implements TimeSampleProvider { + @NonNull + @Override + public List getAllSamples(final long timestampFrom, final long timestampTo) { + final List samples = new ArrayList<>(); + + int spo2 = TestDeviceRand.randInt(timestampFrom, 85, 99); + + for (long ts = timestampFrom; ts < timestampTo; ts += 15 * 60 * 1000L) { + if (TestDeviceRand.randBool(ts, 0.3f)) { + samples.add(new TestSpo2Sample(ts, spo2)); + } + spo2 += TestDeviceRand.randInt(ts, 85 - spo2, 99 - spo2); + } + + return samples; + } + + @Override + public void addSample(final Spo2Sample timeSample) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public void addSamples(final List timeSamples) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public Spo2Sample createSample() { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Nullable + @Override + public Spo2Sample getLatestSample() { + final long ts = System.currentTimeMillis(); + return new TestSpo2Sample( + ts - TestDeviceRand.randLong(ts, 10 * 1000L, 2 * 60 * 60 * 1000L), + TestDeviceRand.randInt(ts, 85, 99) + ); + } + + @Nullable + @Override + public Spo2Sample getFirstSample() { + return new TestSpo2Sample( + TestDeviceRand.BASE_TIMESTAMP, + TestDeviceRand.randInt(TestDeviceRand.BASE_TIMESTAMP, 85, 99) + ); + } + + protected static class TestSpo2Sample implements Spo2Sample { + private final long timestamp; + private final int spo2; + + public TestSpo2Sample(final long timestamp, final int spo2) { + this.timestamp = timestamp; + this.spo2 = spo2; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public Type getType() { + return Type.UNKNOWN; + } + + @Override + public int getSpo2() { + return spo2; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestStressSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestStressSampleProvider.java new file mode 100644 index 000000000..8ae8ec9be --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestStressSampleProvider.java @@ -0,0 +1,105 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test.samples; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceRand; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; + +public class TestStressSampleProvider implements TimeSampleProvider { + @NonNull + @Override + public List getAllSamples(final long timestampFrom, final long timestampTo) { + final List samples = new ArrayList<>(); + + int stress = TestDeviceRand.randInt(timestampFrom, 10, 90); + + for (long ts = timestampFrom; ts < timestampTo; ts += 15 * 60 * 1000L) { + if (TestDeviceRand.randBool(ts, 0.3f)) { + samples.add(new TestStressSample(ts, stress)); + } + stress += TestDeviceRand.randInt(ts, (10 - stress) / 10, (90 - stress) / 10); + } + + return samples; + } + + @Override + public void addSample(final StressSample timeSample) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public void addSamples(final List timeSamples) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public StressSample createSample() { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Nullable + @Override + public StressSample getLatestSample() { + final long ts = System.currentTimeMillis(); + return new TestStressSample( + ts - TestDeviceRand.randLong(ts, 10 * 1000L, 2 * 60 * 60 * 1000L), + TestDeviceRand.randInt(ts, 10, 90) + ); + } + + @Nullable + @Override + public StressSample getFirstSample() { + return new TestStressSample( + TestDeviceRand.BASE_TIMESTAMP, + TestDeviceRand.randInt(TestDeviceRand.BASE_TIMESTAMP, 10, 90) + ); + } + + protected static class TestStressSample implements StressSample { + private final long timestamp; + private final int stress; + + public TestStressSample(final long timestamp, final int stress) { + this.timestamp = timestamp; + this.stress = stress; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public Type getType() { + return Type.UNKNOWN; + } + + @Override + public int getStress() { + return stress; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestTemperatureSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestTemperatureSampleProvider.java new file mode 100644 index 000000000..5dc4abc2e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/samples/TestTemperatureSampleProvider.java @@ -0,0 +1,92 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.test.samples; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceRand; +import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; + +public class TestTemperatureSampleProvider implements TimeSampleProvider { + @NonNull + @Override + public List getAllSamples(final long timestampFrom, final long timestampTo) { + // TODO fake samples + return Collections.emptyList(); + } + + @Override + public void addSample(final TemperatureSample timeSample) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public void addSamples(final List timeSamples) { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Override + public TemperatureSample createSample() { + throw new UnsupportedOperationException("read-only sample provider"); + } + + @Nullable + @Override + public TemperatureSample getLatestSample() { + // TODO + return null; + } + + @Nullable + @Override + public TemperatureSample getFirstSample() { + return new TestTemperatureSample( + TestDeviceRand.BASE_TIMESTAMP, + TestDeviceRand.randFloat(TestDeviceRand.BASE_TIMESTAMP, 36f, 38f) + ); + } + + protected static class TestTemperatureSample implements TemperatureSample { + private final long timestamp; + private final float temperature; + + public TestTemperatureSample(final long timestamp, final float temperature) { + this.timestamp = timestamp; + this.temperature = temperature; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public float getTemperature() { + return temperature; + } + + @Override + public int getTemperatureType() { + return 0; // ? + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/test/TestDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/test/TestDeviceSupport.java new file mode 100644 index 000000000..76ff7cff0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/test/TestDeviceSupport.java @@ -0,0 +1,81 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.test; + +import android.os.Handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.test.TestFeature; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; + +public class TestDeviceSupport extends AbstractDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(TestDeviceSupport.class); + + private final Handler handler = new Handler(); + + @Override + public boolean connect() { + LOG.info("Connecting"); + + getDevice().setState(GBDevice.State.CONNECTING); + getDevice().sendDeviceUpdateIntent(getContext()); + + handler.postDelayed(() -> { + LOG.info("Initialized"); + + if (getCoordinator().supportsLedColor()) { + getDevice().setExtraInfo("led_color", 0xff3061e3); + } else { + getDevice().setExtraInfo("led_color", null); + } + + if (getCoordinator().supports(getDevice(), TestFeature.FM_FREQUENCY)) { + getDevice().setExtraInfo("fm_frequency", 90.2f); + } else { + getDevice().setExtraInfo("fm_frequency", null); + } + + // TODO battery percentages + // TODO hr measurements + // TODO app list + // TODO screenshots + + getDevice().setState(GBDevice.State.INITIALIZED); + getDevice().sendDeviceUpdateIntent(getContext()); + }, 1000); + + return true; + } + + @Override + public void dispose() { + } + + @Override + public boolean useAutoConnect() { + return false; + } + + protected TestDeviceCoordinator getCoordinator() { + return (TestDeviceCoordinator) getDevice().getDeviceCoordinator(); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e78acda7..dc72014a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2679,4 +2679,6 @@ BT_CLASSIC Activity info Could not post ongoing notification due to missing permission + Features + Enabled features for this test device diff --git a/app/src/main/res/xml/devicesettings_test_features.xml b/app/src/main/res/xml/devicesettings_test_features.xml new file mode 100644 index 000000000..4a622203a --- /dev/null +++ b/app/src/main/res/xml/devicesettings_test_features.xml @@ -0,0 +1,12 @@ + + + + +