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 extends Activity> 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 extends ActivitySample> 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 extends Activity> 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 extends DeviceSupport> getDeviceSupportClass() {
- return UnknownDeviceSupport.class;
+ return TestDeviceSupport.class;
}
-
-
@Override
public int getDeviceNameResource() {
return R.string.devicetype_test;
}
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(final GBDevice device, final DaoSession session) {
+ return supportsActivityTracking() ? new TestSampleProvider(device, session) : super.getSampleProvider(device, session);
+ }
+
+ @Override
+ public TimeSampleProvider extends StressSample> 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 extends TemperatureSample> getTemperatureSampleProvider(final GBDevice device, final DaoSession session) {
+ return new TestTemperatureSampleProvider(); // TODO supportsTemperature
+ }
+
+ @Override
+ public TimeSampleProvider extends Spo2Sample> getSpo2SampleProvider(final GBDevice device, final DaoSession session) {
+ return supportsSpo2() ? new TestSpo2SampleProvider() : super.getSpo2SampleProvider(device, session);
+ }
+
+ @Override
+ public TimeSampleProvider extends HeartRateSample> getHeartRateMaxSampleProvider(final GBDevice device, final DaoSession session) {
+ // TODO getHeartRateMaxSampleProvider
+ return super.getHeartRateMaxSampleProvider(device, session);
+ }
+
+ @Override
+ public TimeSampleProvider extends HeartRateSample> getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) {
+ // TODO getHeartRateRestingSampleProvider
+ return super.getHeartRateRestingSampleProvider(device, session);
+ }
+
+ @Override
+ public TimeSampleProvider extends HeartRateSample> getHeartRateManualSampleProvider(final GBDevice device, final DaoSession session) {
+ // TODO getHeartRateManualSampleProvider
+ return super.getHeartRateManualSampleProvider(device, session);
+ }
+
+ @Override
+ public TimeSampleProvider extends PaiSample> getPaiSampleProvider(final GBDevice device, final DaoSession session) {
+ return supportsPai() ? new TestPaiSampleProvider() : super.getPaiSampleProvider(device, session);
+ }
+
+ @Override
+ public TimeSampleProvider extends SleepRespiratoryRateSample> 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 extends Activity> getAppsManagementActivity() {
+ return AppManagerActivity.class;
+ }
+
+ @Nullable
+ @Override
+ public Class extends Activity> 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 extends Activity> 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 @@
+
+
+
+
+