From 921e1f8254e636f104c10c1a958a8fc639dc2d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Wed, 31 Dec 2025 19:30:44 +0000 Subject: [PATCH] GloryFit: Add screen timeout setting --- .../devices/gloryfit/GloryFitCoordinator.kt | 1 + .../gloryfit/GloryFitSettingsCustomizer.kt | 14 ++ .../devices/gloryfit/GloryFitSupport.kt | 15 +- .../gadgetbridge/util/kotlin/IntExtensions.kt | 7 + .../util/preferences/DevicePrefs.java | 4 + .../garmin/fit/messages/FitWeatherTest.java | 12 -- .../util/kotlin/IntExtensionsTest.kt | 135 ++++++++++++++++++ 7 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensions.kt create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensionsTest.kt diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt index 6efe174477..ecc2a67d80 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt @@ -196,6 +196,7 @@ abstract class GloryFitCoordinator : AbstractBLEDeviceCoordinator() { val display = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DISPLAY) display.add(R.xml.devicesettings_liftwrist_display_noshed) + display.add(R.xml.devicesettings_screen_timeout) val health = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.HEALTH) health.add(R.xml.devicesettings_heartrate_automatic_enable) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitSettingsCustomizer.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitSettingsCustomizer.kt index 8673e34f9b..e5234f2f31 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitSettingsCustomizer.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitSettingsCustomizer.kt @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.gloryfit +import androidx.preference.ListPreference import androidx.preference.Preference import kotlinx.parcelize.Parcelize import nodomain.freeyourgadget.gadgetbridge.R @@ -62,6 +63,19 @@ class GloryFitSettingsCustomizer : DeviceSpecificSettingsCustomizer { dndStart?.isVisible = false val dndEnd = handler.findPreference(DeviceSettingsPreferenceConst.PREF_INACTIVITY_DND_END) dndEnd?.isVisible = false + + handler.findPreference(DeviceSettingsPreferenceConst.PREF_SCREEN_TIMEOUT)?.let { + it.entries = arrayOf( + handler.context.getString(R.string.seconds_5), + handler.context.getString(R.string.seconds_10), + handler.context.getString(R.string.seconds_15), + ) + it.entryValues = arrayOf( + "5", + "10", + "15", + ) + } } override fun getPreferenceKeysWithSummary(): Set { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt index 1b70ff6c70..2ed635d0de 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt @@ -59,7 +59,7 @@ import java.util.Calendar import java.util.GregorianCalendar import java.util.Locale import java.util.UUID -import kotlin.math.roundToInt +import nodomain.freeyourgadget.gadgetbridge.util.kotlin.coerceIn class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { init { @@ -944,10 +944,10 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { buf.put(CMD_USER_INFO) buf.putShort(activityUser.heightCm.coerceIn(91, 241).toShort()) buf.putShort(activityUser.weightKg.coerceIn(20, 255).toShort()) - buf.put(0x05) // ? + buf.put(devicePrefs.screenTimeout.coerceIn(5, 15, 5).toByte()) buf.put(0x00) // ? buf.put(0x00) // ? - buf.putShort(((activityUser.stepsGoal / 1000.0).roundToInt() * 1000).coerceIn(1000, 30000).toShort()) + buf.putShort(activityUser.stepsGoal.coerceIn(1000, 30000, 1000).toShort()) buf.put(if (raiseHandToActivateDisplay) 0x01 else 0x00) buf.put(heartRateAlertHigh) buf.put(0x00) // ? @@ -970,7 +970,7 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { fun setGoalSteps(builder: TransactionBuilder) { val activityUser = ActivityUser() - val stepsCoerced = ((activityUser.stepsGoal / 1000.0).roundToInt() * 1000).coerceIn(1000, 30000).toShort() + val stepsCoerced = activityUser.stepsGoal.coerceIn(1000, 30000, 1000).toShort() LOG.debug("Setting steps goal to {}", stepsCoerced) @@ -988,7 +988,7 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { fun setGoalCalories(builder: TransactionBuilder) { val activityUser = ActivityUser() - val caloriesCoerced = ((activityUser.caloriesBurntGoal / 50.0).roundToInt() * 50).coerceIn(50, 1000).toShort() + val caloriesCoerced = activityUser.caloriesBurntGoal.coerceIn(50, 1000, 50).toShort() LOG.debug("Setting calories goal to {}", caloriesCoerced) @@ -1004,8 +1004,7 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { fun setGoalDistance(builder: TransactionBuilder) { val activityUser = ActivityUser() - val distanceCoerced = - ((activityUser.distanceGoalMeters / 1000f).roundToInt() * 1000).coerceIn(1000, 20000).toShort() + val distanceCoerced = activityUser.distanceGoalMeters.coerceIn(1000, 20000, 1000).toShort() LOG.debug("Setting distance goal to {}", distanceCoerced) @@ -1037,7 +1036,7 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { val buf = ByteBuffer.allocate(12).order(ByteOrder.BIG_ENDIAN) buf.put(CMD_SEDENTARY_REMINDER) buf.put(if (enabled) 0x01 else 0x00) - buf.put(((duration / 5.0).roundToInt() * 5).coerceIn(30, 180).toByte()) + buf.put(duration.coerceIn(30, 180, 5).toByte()) buf.put(0x02) // ? buf.put(0x03) // ? buf.put(0x01) // ? diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensions.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensions.kt new file mode 100644 index 0000000000..629bcbf56e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensions.kt @@ -0,0 +1,7 @@ +package nodomain.freeyourgadget.gadgetbridge.util.kotlin + +import kotlin.math.roundToInt + +fun Int.coerceIn(min: Int, max: Int, step: Int): Int { + return ((this / step.toDouble()).roundToInt() * step).coerceIn(min, max) +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/preferences/DevicePrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/preferences/DevicePrefs.java index fa6fb98f3b..7333a3f952 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/preferences/DevicePrefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/preferences/DevicePrefs.java @@ -156,4 +156,8 @@ public class DevicePrefs extends Prefs { public int getHeartRateLowThreshold() { return getInt(DeviceSettingsPreferenceConst.PREF_HEARTRATE_ALERT_LOW_THRESHOLD, 0); } + + public int getScreenTimeout() { + return getInt(PREF_SCREEN_TIMEOUT, 5); + } } diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java index b93acb6fea..264e39343c 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java @@ -1,32 +1,20 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages; -import static org.junit.Assert.*; - import androidx.annotation.NonNull; import org.junit.Assert; import org.junit.Test; -import java.time.Instant; -import java.time.ZoneId; import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import nodomain.freeyourgadget.gadgetbridge.activities.DebugActivity; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; -import nodomain.freeyourgadget.gadgetbridge.model.weather.WeatherMapper; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitLocalMessageBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherAqi; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherCondition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; import nodomain.freeyourgadget.gadgetbridge.util.GB; diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensionsTest.kt b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensionsTest.kt new file mode 100644 index 0000000000..ea1e6fbcfa --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/IntExtensionsTest.kt @@ -0,0 +1,135 @@ +package nodomain.freeyourgadget.gadgetbridge.util.kotlin + +import org.junit.Assert.assertEquals +import org.junit.Test + +class IntExtensionsTest { + @Test + fun testCoerceInWithStep_valueWithinRange() { + // Value already on a step boundary within range + assertEquals(50, 50.coerceIn(0, 100, 10)) + + // Value within range but not on step boundary - should round to nearest step + assertEquals(50, 52.coerceIn(0, 100, 10)) + assertEquals(50, 48.coerceIn(0, 100, 10)) + } + + @Test + fun testCoerceInWithStep_valueBelowMin() { + // Value below minimum should be coerced to minimum (respecting step) + assertEquals(0, (-10).coerceIn(0, 100, 10)) + assertEquals(10, 5.coerceIn(10, 100, 10)) + } + + @Test + fun testCoerceInWithStep_valueAboveMax() { + // Value above maximum should be coerced to maximum + assertEquals(100, 150.coerceIn(0, 100, 10)) + assertEquals(100, 105.coerceIn(0, 100, 10)) + } + + @Test + fun testCoerceInWithStep_roundingToNearestStep() { + // Test rounding behavior: rounds to nearest step + assertEquals(10, 6.coerceIn(0, 100, 10)) + assertEquals(10, 7.coerceIn(0, 100, 10)) + assertEquals(10, 8.coerceIn(0, 100, 10)) + assertEquals(10, 9.coerceIn(0, 100, 10)) + assertEquals(10, 10.coerceIn(0, 100, 10)) + assertEquals(10, 11.coerceIn(0, 100, 10)) + assertEquals(10, 12.coerceIn(0, 100, 10)) + assertEquals(10, 13.coerceIn(0, 100, 10)) + assertEquals(10, 14.coerceIn(0, 100, 10)) + // > 15 should round to 20 + assertEquals(20, 15.coerceIn(0, 100, 10)) + assertEquals(20, 16.coerceIn(0, 100, 10)) + assertEquals(20, 17.coerceIn(0, 100, 10)) + assertEquals(20, 18.coerceIn(0, 100, 10)) + assertEquals(20, 19.coerceIn(0, 100, 10)) + } + + @Test + fun testCoerceInWithStep_differentStepSizes() { + // Test with step size of 5 + assertEquals(15, 14.coerceIn(0, 100, 5)) + assertEquals(15, 16.coerceIn(0, 100, 5)) + + // Test with step size of 25 + assertEquals(25, 24.coerceIn(0, 100, 25)) + assertEquals(25, 30.coerceIn(0, 100, 25)) + + // Test with step size of 1 (should behave like normal coerceIn) + assertEquals(42, 42.coerceIn(0, 100, 1)) + assertEquals(10, 10.coerceIn(0, 100, 1)) + assertEquals(15, 15.coerceIn(0, 100, 1)) + assertEquals(0, (-5).coerceIn(0, 100, 1)) + assertEquals(100, 105.coerceIn(0, 100, 1)) + } + + @Test + fun testCoerceInWithStep_negativeRanges() { + // Test with negative min and max + assertEquals(-50, (-52).coerceIn(-100, 0, 10)) + assertEquals(-50, (-48).coerceIn(-100, 0, 10)) + assertEquals(-100, (-150).coerceIn(-100, 0, 10)) + assertEquals(0, 50.coerceIn(-100, 0, 10)) + } + + @Test + fun testCoerceInWithStep_rangeSpanningZero() { + // Test with range spanning negative to positive + assertEquals(0, 2.coerceIn(-50, 50, 10)) + assertEquals(10, 12.coerceIn(-50, 50, 10)) + assertEquals(-10, (-12).coerceIn(-50, 50, 10)) + } + + @Test + fun testCoerceInWithStep_edgeCases() { + // Value exactly at minimum + assertEquals(0, 0.coerceIn(0, 100, 10)) + + // Value exactly at maximum + assertEquals(100, 100.coerceIn(0, 100, 10)) + + // Single value range (min == max) + assertEquals(50, 50.coerceIn(50, 50, 10)) + assertEquals(50, 45.coerceIn(50, 50, 10)) + assertEquals(50, 55.coerceIn(50, 50, 10)) + } + + @Test + fun testCoerceInWithStep_largeStepSize() { + // Step size larger than range + assertEquals(0, 25.coerceIn(0, 50, 100)) + assertEquals(0, 49.coerceIn(0, 50, 100)) + assertEquals(50, 51.coerceIn(0, 50, 100)) + assertEquals(50, 99.coerceIn(0, 50, 100)) + assertEquals(50, 101.coerceIn(0, 50, 100)) + assertEquals(50, 150.coerceIn(0, 50, 100)) + assertEquals(50, 200.coerceIn(0, 50, 100)) + assertEquals(50, 250.coerceIn(0, 50, 100)) + assertEquals(0, (-10).coerceIn(0, 50, 100)) + } + + @Test + fun testCoerceInWithStep_smallValues() { + // Test with small ranges and steps + assertEquals(0, 0.coerceIn(0, 10, 2)) + assertEquals(2, 1.coerceIn(0, 10, 2)) + assertEquals(2, 2.coerceIn(0, 10, 2)) + assertEquals(4, 3.coerceIn(0, 10, 2)) + assertEquals(4, 4.coerceIn(0, 10, 2)) + assertEquals(6, 5.coerceIn(0, 10, 2)) + assertEquals(6, 6.coerceIn(0, 10, 2)) + assertEquals(8, 7.coerceIn(0, 10, 2)) + assertEquals(8, 8.coerceIn(0, 10, 2)) + } + + @Test + fun testCoerceInWithStep_maxNotOnStepBoundary() { + // When max is not on a step boundary + // e.g., 0, 10, 20, 30, 40, 50... but max is 45 + assertEquals(40, 42.coerceIn(0, 45, 10)) + assertEquals(45, 48.coerceIn(0, 45, 10)) // Rounded to 50, coerced to 45 + } +}